More updates on my Mix09 talk “building business applications with Silverlight 3”.
You can watch the original video of the full session
The demo requires (all 100% free and always free):
Also, download the full demo files…
In the original demo I showed getting your data from a database directly from the web tier. Many enterprise customers have found that it their systems are more maintainable and secure if they isolate the database access behind a set of web services possibly in a DMZ. This means that no apps talk directly to the database. Yet there is often a need to add application specific validation and application logic as well as data shaping and aggregation in the web-tier.
To show this, I have refactored the example application from the first part of this walk through to access its data from a WCF service rather than EF directly. Notice that all the other UI bits stayed the same,
Let’s start by defining the WCF service. In a real word application this service is likely defined by another group and you are only allowed to access it, not modify it.
Right click on the solution and add a new project… WCF Service Application. I called it MyApp.Service, but you can choose anything you’d like.
First we create an Entity Framework model for our database.. this is done exactly the same way as part 2, but this time it is part of our Service rather than in the web-tier. The demo would work exactly the same no mater what source of data you use… EF was just easy for me to get going, it is not required for this scenario as we are encapsulating everything behind a the WCF services layer.
Next, we define the interface for our service
[ServiceContract]
public interface ISuperEmployeeService
{
[OperationContract]
IEnumerable<SuperEmployee> GetSuperEmployees(int page);
SuperEmployee GetSuperEmployee(int empId);
void UpdateEmployee(SuperEmployee emp);
}
Then we implement it…
public IEnumerable<SuperEmployee> GetSuperEmployees(int page)
using (var context = new NORTHWNDEntities()) {
var q = context.SuperEmployeeSet
.OrderBy(emp=>emp.EmployeeID)
.Skip(page * PageSize).Take(PageSize);
return q.ToList();
Notice here we are implementing paging by taking a page parameter… After a brief inventory of real world services on the net, i find this a very common pattern. It is very easy with EF to access just the page of data we want.
Now let’s consume this service from the web-tier. The reason we do this here is to get all the benefits of the RIA Services in terms of validation logic, etc and be able to customize and arrogate the data so the view is just right for the client.
First, we define a SuperEmployee type that is shaped just right for the client. In this simple example I left it pretty much the same as the service returned, but you can use any shape you’d like.
Notice we are using attributes to specify the different validation we want to have done on this data on the client AND the web-tier.
public class SuperEmployee
[ReadOnly(true)]
[Key]
public int EmployeeID { get; set; }
[RegularExpression("^(?:m|M|male|Male|f|F|female|Female)$",
ErrorMessage = "Gender must be 'Male' or 'Female'")]
public string Gender {get;set;}
[Range(0, 10000,
ErrorMessage = "Issues must be between 0 and 1000")]
public Nullable<int> Issues {get;set;}
public Nullable<DateTime> LastEdit {get;set;}
[Required]
[StringLength(100)]
public string Name {get;set;}
public string Origin {get;set;}
public string Publishers {get;set;}
public string Sites {get;set;}
Now, let’s add a reference to the service we just created.
Right click on the project and select Add Service Reference..
Now we modify our DomainService…
1: public class SuperEmployeeDomainService : DomainService
2: {
3: SuperEmployeeServiceClient Context = new SuperEmployeeServiceClient();
4:
5: public IQueryable<SuperEmployee> GetSuperEmployees(int pageNumber)
6: {
7: return this.Context.GetSuperEmployees(pageNumber)
8: .Where(emp => emp.Issues > 100)
9: .OrderBy(emp => emp.EmployeeID)
10: .Select(emp =>
11: new MyApp.Web.SuperEmployee()
12: {
13: EmployeeID = emp.EmployeeID,
14: Gender = emp.Gender,
15: Issues = emp.Issues,
16: LastEdit = emp.LastEdit,
17: Name = emp.Name,
18: Origin = emp.Origin,
19: Publishers = emp.Publishers,
20: Sites = emp.Sites,
21: }).AsQueryable();
22: }
Notice in line 1, we no longer need to derive from EFDomainService as there is no DAL access code in this web-tier now… we have factored all that into the service.
In line 3, we create an instance of the WCF web Service Proxy..
In line 5, you can see we are taking a page number – we will need to pass that from the client.
In line 7 we are creating a simple LINQ query to change the shape of the data we get back from the service.
Now, to consume that in the silverlight client is very easy.. We just make a few tweaks to the Home.xaml we created in part 2..
1: <riaControls:DomainDataSource x:Name="dds"
2: AutoLoad="True"
3: QueryName="GetSuperEmployeesQuery"
4: LoadSize="20">
5:
6: <riaControls:DomainDataSource.QueryParameters>
7: <datagroup:ControlParameter ParameterName="pageNumber"
8: ControlName="pager"
9: RefreshEventName="PageIndexChanged"
10: PropertyName="PageIndex">
11: </datagroup:ControlParameter>
12:
13: </riaControls:DomainDataSource.QueryParameters>
14:
15: <riaControls:DomainDataSource.DomainContext>
16: <App:SuperEmployeeDomainContext/>
17: </riaControls:DomainDataSource.DomainContext>
18:
19: <riaControls:DomainDataSource.GroupDescriptors>
20: <datagroup:GroupDescriptor PropertyPath="Publishers" />
21: </riaControls:DomainDataSource.GroupDescriptors>
22:
23:
24:
25: </riaControls:DomainDataSource>
26:
Notice in line 7 we are getting the page number from the GetSuperEmployees() method from the DataPager… This is set up such that as the DataPager changes pages, the DDS request data from the server.
If you are paying close attention to the whole blog series, you will notice I removed the sorting and filtering for this example. I did this because you are basically limited by the expressiveness of your back-end data… If your back end data does not support sorting and filtering, then it is tough to do that on the client! You could do caching on the web tier but that is a subject for another day.
As an fun exercise, assume you could change the WCF Service definition, how would you add filtering? You could follow the exact pattern we use for paging. Add an argument to the WCF Service, add the same one to the GetSuperEmployees() query method on the server, then add another ControlParamater to the DDS getting its data from a control on the form. Pretty easy! Let me know if you try it.
The final step is to bind the DataPager to a shim collection, just to make it advance.
1: <data:DataPager x:Name="pager" PageSize="1" Width="379"
2: HorizontalAlignment="Left"
3: DisplayMode="FirstLastPreviousNext"
4: IsEnabled="True"
5: IsTotalItemCountFixed="False"
6: Margin="0,0.2,0,0">
7: <data:DataPager.Source>
8: <paging:PagingShim PageCount="-1"></paging:PagingShim>
9: </data:DataPager.Source>
10: </data:DataPager>
You can find the implementation of PagingShim in the sample. (thanks to David Poll for his help with it).
Hit F5, and we have something cool! It looks pretty much the same as the previous app, but this one gets all its data via WCF!
Excellent - thanks Brad. I personally think this is a common scenario - so it's good to see post on it.
Loving the series, thanks again
Thanks for the serie!
But, Where is the part 7? or, I'm lost...?
> But, Where is the part 7? or, I'm lost...?
Mike -- Ahh, yes.. mysterious post 7... well, I sort of screwed up and posted 8 before 7... Not sure how I will resolve that yet.. i might just post 7 as 9 and shift everything down.. thoughts?
Post 7 as 7 :) Leave 8 as 8 and 9 will just have to stay 9
Hi,
This article is very nice. I have to try this .
Thanks,
thani
Great series.
Could you provide an example of how to hookup master-detail (Customers-Orders) using data binding with the Silverlight DomainDataSource and a pair of DataGrids. Specifically how do you reference, say the customer number for the SelectedItem in the Customers grid to use as a parameter value in the Orders grid.
Can you have more that one DomainDataSource for the same DataContext? I can see how to do this in code but not xaml.
Any thoughts would be appreciated.
Thanks.
Brad, I'd liked to see your sample done with Prism - showing Prism with DomainServices would be a good topic in your series :)
> Could you provide an example of how to hookup > master-detail (Customers-Orders) using data
> binding with the Silverlight DomainDataSource
Mark -- I will look into an example like that -- in the mean time, here is the section from the RIAServices overview doc that includes information on the IncludeAttribute.. That should help.
4.8 Using the IncludeAttribute
The IncludeAttribute can be applied to association members to shape the generated client entities as well as to influence serialization. Below we explore two uses of this attribute.
4.8.1 Returning Related Entities
When querying entity Types with associations to other entities, often you’ll want to return the associated entities along with the top level entities returned by the query. For example, assume your DomainService exposes Products, and for each Product returned you also want to return its associated ProductSubCategory. The first thing to do is ensure that the associated entities are actually returned from the data source when Products are queried. The mechanism used to load the associated entities is a DAL specific thing. In this LINQ to SQL example, the code would be:
public IQueryable<Product> GetProducts()
DataLoadOptions loadOpts = new DataLoadOptions();
loadOpts.LoadWith<Product>(p => p.ProductSubcategory);
this.Context.LoadOptions = loadOpts;
return this.Context.Products;
With this code, all ProductSubCategories are returned from the DAL when Products are queried. The next step is to indicate to .NET RIA Services that a ProductSubCategory entity should be code generated, as should the bi-directional association between the two Types. To do this you need to apply the IncludeAttribute to the association member.
IncludeAttribute has both serialization semantics and data-shape semantics. For both serialization of results to the client as well as the client object model we code-gen, .NET RIA Services will only ever expose data and types from the service that you have explicitly exposed from your service.
• If a Type is exposed via a query method from the service a corresponding proxy class will be generated on the client for that Type. In addition any associations referencing that Type from other exposed types will also be generated.
• If a Type is not exposed by a query method, but there is an association on another exposed Type that is marked with IncludeAttribute, a corresponding proxy class will be generated on the client for that Type.
Those points define what types are defined on the client and their shapes. In addition to these data shape semantics, IncludeAttribute also has serialization semantics : during serialization, only associations marked with IncludeAttribute will actually be traversed. All of this ensures that only information you've explicitly decided to expose is exposed.
Therefore, we need to apply the IncludeAttribute to the Product.Category association member. We do this by adding the attribute to the buddy metadata class:
internal sealed class ProductMetadata
[Include]
public ProductSubcategory ProductSubcategory;
With this in place we’ll get a ProductSubCategory entity generated on the client, as well as the Product.ProductSubCategory association member. This works the same way for collection associations. For example, to include all Products when querying a ProductSubCategory, you’d put the IncludeAttribute on the ProductSubCategory.Products member.
4.8.2 Denormalizing Associated Data
IncludeAttribute can also be used for another purpose – data denormalization. For example, assume that in the above scenario we don’t really need the entire ProductSubCategory for each Product, we only want the Name of the category.
[Include("Name", "SubCategoryName")]
The above attribute results in a “SubCategoryName” member being generated on the client Product entity. When querying Products, only the category Name is sent to the client, not the entire category. Note that the ProductSubCategory must still be loaded from the DAL as it was in the previous example.
Brad: How do you deal with authentication delegation here, specifically within a Windows-auth domain model?
For example:
Client (web browser) is on Machine A.
Web Tier is on Machine B.
Database (via EF) (or wcf service on database) is on Machine C.
Only with Active Directory delegation can the user credentials make it all the way from Machine A to Machine C since they'll be going via Machine B.
What's the recommended security model if you can't setup delegation in this case? Does the database need to be connected via SQL logins? How do you deal with web services on other machines that require Windows-auth?
We've really been struggling with this since we're not able to setup delegation in our domain.
Brad,
I created a sample application using the pattern you described in tihs post. When i page it gets me data till 6th page and after that it spins for a while with activity bar shown eventually it stops with System.TimeoutException.
When i debugged i found that in the generated class it never makes a call to the WCF service.
public EntityQuery<ZipCode> GetZipCodesQuery(int pageNumber)
Dictionary<string, object> parameters = new Dictionary<string, object>();
parameters.Add("pageNumber", pageNumber);
return base.CreateQuery<ZipCode>("GetZipCodes", parameters, false, true);
I had a breakpoint in the WCF service
IEnumerable<ZipCode> IZipCodeService.GetZipCodes(int page)
var context = new Mid_MarketEntities();
var q = context.ZipCode.OrderBy(zip => zip.ZipID).
Skip(page * PageSize).Take(PageSize);
but it never made into this function after the 6th page.
Not sure what am I missing. Is there a way to add trace to RIA services to troubleshoot such kind of problems.
Tazul
I did see this a couple of times and I attributed it to the development-web server in VS... Are you using that or IIS? Can you let me know if this happens under IIS as well?
Thanks for the reply. I am using development web server. I haven't tried in IIS. Do you think this will not occur in IIS?
I tried after deploying both the WCF service and Web project on IIS and I am having same problem. I used 20 records in DDS and same in the WCF service.
<riaControls:DomainDataSource x:Name="dds"
AutoLoad="True"
QueryName="GetSuperEmployeesQuery"
LoadSize="20">
<riaControls:DomainDataSource.QueryParameters>
<datagroup:ControlParameter ParameterName="pageNumber"
ControlName="pager"
RefreshEventName="PageIndexChanged"
PropertyName="PageIndex">
</datagroup:ControlParameter>
</riaControls:DomainDataSource.QueryParameters>
<riaControls:DomainDataSource.DomainContext>
<App:SuperEmployeeDomainContext/>
</riaControls:DomainDataSource.DomainContext>
<riaControls:DomainDataSource.GroupDescriptors>
<datagroup:GroupDescriptor PropertyPath="Publishers" />
</riaControls:DomainDataSource.GroupDescriptors>
</riaControls:DomainDataSource>
WCF Service:
const int PageSize = 20;
var context = new NORTHWINDEntities();
.OrderBy(emp => emp.EmployeeID)
After the 5th page (100 records) when i click next, it does the same thing. I tried changing
the pagesize and the load size but after fetching around 100-150 records it fails.
I found the problem and fixed it. It was with WCF service. Domain Service was not closing the channel and was open. By default WCF has 10 max concurrent sessions. While paging Domain service makes calls to the WCF service to fetch next batch but doesn't close the channel. So I made a change to the WCF function to close the channel after the call is done.
context.Close(); //This fixed the problem.
Excellent! thanks... I will fix my sample.