次の方法で共有


Business Apps Example for Silverlight 3 RTM and .NET RIA Services July Update: Part 8: WCF Based Data Source

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):

  1. VS2008 SP1 (Which includes Sql Express 2008)
  2. Silverlight 3 RTM
  3. .NET RIA Services July '09 Preview

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,

image

 

Defining the Service

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.

image

 

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);
  
     [OperationContract]
      SuperEmployee GetSuperEmployee(int empId);
  
  
     [OperationContract]
     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. 

 

Consuming the Service

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..

image

 

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. 

 

 

In the Silverlight Client

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!

image

Comments

  • Anonymous
    July 17, 2009
    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

  • Anonymous
    July 18, 2009
    Thanks for the serie! But, Where is the part 7? or, I'm lost...?

  • Anonymous
    July 18, 2009
    The comment has been removed

  • Anonymous
    July 18, 2009
    Post 7 as 7  :)  Leave 8 as 8 and 9 will just have to stay 9

  • Anonymous
    July 19, 2009
    Hi, This article is very nice. I have to try this . Thanks, thani

  • Anonymous
    July 19, 2009
    The comment has been removed

  • Anonymous
    July 20, 2009
    Brad, I'd liked to see your sample done with Prism - showing Prism with DomainServices would be a good topic in your series  :)

  • Anonymous
    July 20, 2009
    The comment has been removed

  • Anonymous
    July 20, 2009
    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.

  • Anonymous
    August 18, 2009
    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);            return q.ToList();        } 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. Thanks, Tazul

  • Anonymous
    August 18, 2009
    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?

  • Anonymous
    August 19, 2009
    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?  

  • Anonymous
    August 19, 2009
    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;        public IEnumerable<SuperEmployee> GetSuperEmployees(int page)        {            var context = new NORTHWINDEntities();            var q = context.SuperEmployeeSet                .OrderBy(emp => emp.EmployeeID)                .Skip(page * PageSize).Take(PageSize);            return q.ToList();        } 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.

  • Anonymous
    August 19, 2009
    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. WCF Service:       const int PageSize = 20;       public IEnumerable<SuperEmployee> GetSuperEmployees(int page)       {           var context = new NORTHWINDEntities();           var q = context.SuperEmployeeSet               .OrderBy(emp => emp.EmployeeID)               .Skip(page * PageSize).Take(PageSize);           context.Close(); //This fixed the problem.           return q.ToList();       }

  • Anonymous
    August 19, 2009
    Excellent!  thanks... I will fix my sample.

  • Anonymous
    September 14, 2009
    Hi BradA, In your code you have Update and Insert functions public void InsertSuperEmployee(SuperEmployee superEmployee)        {            Context.UpdateEmployee(Convert(superEmployee));        }        public void UpdateSuperEmployee(SuperEmployee currentSuperEmployee)        {            Context.UpdateEmployee(Convert(currentSuperEmployee));        } but you never called these on client. Your these functions are not generated on client as they are not IQuerable nor they carry the Attribute Query with them, i mean they donot fulfill the requirements of getting gnerated on the client, neither are they generating in your code. My question is in this scenario what are your plans on how to call these domain methods from the client code.

  • Anonymous
    September 14, 2009
    Aashish Gupta  - Those are called when you are add a new SuperEmployee or update data on an super employee.  They are part of the changeset processing.  

  • Anonymous
    September 24, 2009
    Hi, It would be great if you can shed any light on this... The  sort properties on a domaindatasource are lost after a paging operation The properties are there , but they are not present in the final query on the sql server and thus the data is no longer sorted.. any ideas on how to fix this ??? I have already created a thread here on silverlight.net forum http://forums.silverlight.net/forums/p/130739/292189.aspx appreciate any kind of help  

  • Anonymous
    October 09, 2009
    Thanks for this series Brad - I'm really learning alot. Some feedback - I find anything that's not strictly presentation living in the Xaml to be extremely confusing and hard to follow.  I'm sure there are other ways to do the same thing (that are probably less demo-friendly), but more straightforward.  Xaml in general is very difficult to read if you're not an expert - especially if its written in one of the many abbreviated ways it allows. I've been trying to take one of these MyApp drops and steal from it piece by piece to get a simple app of my own up and running.  Things like this in the Xaml are very difficult to understand: <riaControls:DomainDataSource.DomainContext>    <App:SuperEmployeeDomainContext/> </riaControls:DomainDataSource.DomainContext> <riaControls:DomainDataSource.GroupDescriptors>    <datagroup:GroupDescriptor PropertyPath="Publishers" /> </riaControls:DomainDataSource.GroupDescriptors> I know there's some kind of context object that RIA Service is generating for me, but it's really difficult for me to understand how to change this to get it working with my service.  This feels like that whole ObjectDataSource thing from ASP.NET that I refused to take part in.  Please tell me there are ways to do this in the code behind with more of a MVP or MV-VM pattern? Thanks for all this content!