RIA Services: A DomainService IS A WCF Service – Add Service Reference

I made the fairly bold statement at my PDC09 talk that a DomainService IS A WCF Service.  That is, everything you know about a WCF service should be true of a DomainService.  I didn’t have time to get into this in my talk, so I thought I’d hit the highlights here.  And in the process show how to consume a DomainService from a WinForms.    You can also see more examples at: https://code.msdn.microsoft.com/RiaServices 

You need:

 

You can download the completed solution as well.  and be sure to check out the full talk

 

1. Getting to the Service

The first thing we need to do is get at the data underlying service.  In the mainstream Silverlight case this is all handled for you by the implicit link between the Silverlight client and the ASP.NET server.  However, in the vanilla WCF case, you get the full control.  The URL to the service is of the following format:

https://[hostname]/[namespacename]-[classname].svc

so in my case that is:

https://localhost:30335/Services/MyApp-Web-DishViewDomainService.svc

Hitting that URL in the browser gives you the very familiar WCF proxy help screen:

image

And tacking on the ?wsdl gives you the WSDL for this service

https://localhost:30335/Services/MyApp-Web-DishViewDomainService.svc?wsdl

 

image

 

The rest is easy for anyone halfway familiar with WCF… Create a new WinForms project and select Add Service Reference.  Enter the URL (note discover doesn’t work for this sort of service yet)…

image

The you have a service! 

 

 

2. Querying for the Data

Now, we have a service, let’s look at actually getting data out of it.  In this case I already have a WinForms DataGridView on my form.  So getting data into it should be no problem. 

  1. private void Form1_Load(object sender, EventArgs e)
  2. {
  3.     var context = new DishViewDomainServiceClient("BasicHttpBinding_DishViewDomainService");
  4.     var plates = context.GetPlates(4);
  5.     this.dataGridView1.DataSource = plates.RootResults;
  6.     foreach (DataGridViewRow row in dataGridView1.Rows)
  7.     {
  8.         PlatesListOriginals.Add(ToPlate(row));
  9.     }
  10.     dataGridView1.CellEndEdit += dataGridView1_CellEndEdit;
  11.     dataGridView1.SelectionChanged += dataGridView1_SelectionChanged;
  12. }
  13.  

In line 3, we create a new instance of the web service client and point it at the right binding.    The service exposes a couple of different bindings as you can see in the app.config file for the WinForms app:

  1. <endpoint address="https://localhost:30335/Services/MyApp-Web-DishViewDomainService.svc/soap"
  2.     binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_DishViewDomainService"
  3.     contract="ServiceReference1.DishViewDomainService" name="BasicHttpBinding_DishViewDomainService" />
  4. <endpoint address="https://localhost:30335/Services/MyApp-Web-DishViewDomainService.svc/binary"
  5.     binding="customBinding" bindingConfiguration="BinaryHttpBinding_DishViewDomainService"
  6.     contract="ServiceReference1.DishViewDomainService" name="BinaryHttpBinding_DishViewDomainService" />
  7.  

In line 4, we call the service to get our list of Plates… in this case we are doing things synchronously.. you could of course do it async if you’d like.

In line  5, we bind the DataGridView to the results of this call.

In lines 6-9, we are saving off the “original” values..  for each item we got.. this will help us when we do updates.

In line 10, we handle the cell edit event, we will come back to look at that later.

in line 11, we sign up for the selection changed event so we can initialize the picture…

  1. void dataGridView1_SelectionChanged(object sender, EventArgs e)
  2. {
  3.      Plate currentPlate = ToPlate(dataGridView1.CurrentRow);
  4.      this.pictureBox1.ImageLocation = "https://hanselman.com/abrams/Images/Plates/" + currentPlate.ImagePath;
  5. }
  6.  

Be patient with this one… sometimes it takes a while load a picture.  it is using hanselman’s server which gets slammed sometimes ;-)

image

Now we have our data, we can scroll through it and view the pretty pictures. 

 

3. Updating the Data

But how do we update the data… well, let’s take a look at CellEditEnd event handler…

  1. void dataGridView1_CellEndEdit(object sender, DataGridViewCellEventArgs e)
  2. {
  3.     var context = new DishViewDomainServiceClient("BasicHttpBinding_DishViewDomainService");
  4.     Plate currentPlate = ToPlate(dataGridView1.Rows[e.RowIndex]);
  5.     ChangeSetEntry[] changeSet = new[] {
  6.         new ChangeSetEntry{
  7.          OriginalEntity = PlatesListOriginals[e.RowIndex],   
  8.          Entity = currentPlate,
  9.          Operation = DomainOperation.Update
  10.         }
  11.     };
  12.     context.SubmitChanges(changeSet);
  13. }
  14.  

 

In line 3, we are creating a new context.  we could be sharing with the load method, but I thought this would be cleaner to follow.

In line 4, we save off the currently selected plate.

In lines 5-10 we are building up a changeset to send to the server. 

Notice we need to give it the original values we saved off in the load method.    Getting the original values right is the likely the hardest part here.  Keep in mind that assignment in C# (and VB) is by default by reference.  So you can’t just store off a reference, you must actually make a copy of the original values.

Then in line 12, we submit the changes. 

Make a change, tab off it..  This will call the server and post your update.   Re run the app to see that it took.

image

Notice here we are sending one item in the change set.  You could of course build up a change set on the client with many entries and then send them as a batch. 

 

I hope that helps to make it clear how a DomainService IS A WCF Service… You can download the completed solution as well.  and be sure to check out the full talk.

Comments

  • Anonymous
    November 23, 2009
    Is there a way to use dynamically loaded assemblies for domain services ? If so how to do that?

  • Anonymous
    November 23, 2009
    MyApp.Web project References dot find System.Web.DomainServices.EntityFramework, why? i use vs2010beta2, silverlight4beta.

  • Anonymous
    November 24, 2009
    The comment has been removed

  • Anonymous
    November 25, 2009
    Thanks for the update Mr. Abrams. I'm wondering if you could give us a quick lesson on how to implement Inserting and not just updating? I've been looking around but I haven't figured out how to tell to my DataGrid or Details control to add a new record.

  • Anonymous
    December 15, 2009
    Is there a way to hide the WSDL so that it is not served up to prying eyes?  We have "RequiresAuthentication" decorated on many of our methods and it would be nice if users didn't even know what the names of the methods were in the first place.  Any way to hide the WSDL or specific methods in the WSDL?

  • Anonymous
    December 15, 2009
    Good point Ben, we are looking at turning off the metadata by default.. Love to have feedback on that.   For now, you’ll have to plug-in a custom DomainServiceHost and override AddDefaultBehaviors:                base.AddDefaultBehaviors();                this.Description.Find<ServiceMetadataBehavior>().HttpGetEnabled = false;

  • Anonymous
    December 16, 2009
    The comment has been removed

  • Anonymous
    December 18, 2009
    Can you provide an example of calling a domain service from ajax?

  • Anonymous
    December 19, 2009
    Is there a way to use the DomainDataSource as well?  Meaning over WCF.

  • Anonymous
    December 22, 2009
    Hi, I've done the same way as per blog. But, I used the service asynchonously like - private CustDomainService.CustDomainServiceClient objClient = new prjTestRIAservice.CustDomainService.CustDomainServiceClient("BasicHttpBinding_CustDomainService");                public MainPage()        {            InitializeComponent();                        objClient.GetCustomersAsync();            objClient.GetCustomersCompleted += new EventHandler<prjTestRIAservice.CustDomainService.GetCustomersCompletedEventArgs>(objClient_GetCustomersCompleted);        }        void objClient_GetCustomersCompleted(object sender, prjTestRIAservice.CustDomainService.GetCustomersCompletedEventArgs e)        {            var lstdata = e.Result;            grdTest.ItemsSource = lstdata.RootResults;        }


I'm getting error like - 'prjTestRIAservice.CustDomainService.QueryResultOfCustomers' does not implement inherited abstract member 'System.Windows.Ria.Services.QueryResult.GetIncludedResults()' and 'prjTestRIAservice.CustDomainService.QueryResultOfCustomers' does not implement inherited abstract member 'System.Windows.Ria.Services.QueryResult.GetRootResults()'

  • Anonymous
    December 24, 2009
    I'm also running up against the same problem as pragati.  My situation requires mt tohhost the domain services in a completely seperate web site from the silverlight app.  When I try to add the domain service as a service reference to the silverlight app, I get the above mentioned errors.  The domain service work fine in a traditional asp page amd wcf service when added as a service reference.

  • Anonymous
    December 27, 2009
    Hi rjacobs, please let me know the solution if you get any. My email id is - pragati.dukale@revalanalytics.com I'm still with the same problem for SL appln.

  • Anonymous
    December 29, 2009
    Hi, Is there a way to host a domainService in a console app or a windows service. I've been through all the web to find something about this topic. thank you.

  • Anonymous
    December 29, 2009
    Hi, Is there a way to host a domainService in a console app or a windows service ? I've been through all the web to find something about this topic. thank you.

  • Anonymous
    December 31, 2009
    I'm getting that problem that results in this error: An ExceptionDetail, likely created by IncludeExceptionDetailInFaults=true, whose value is: System.NullReferenceException: Object reference not set to an instance of an object.   at System.ServiceModel.Description.ServiceMetadataBehavior.MetadataExtensionInitializer.GenerateMetadata() According to Google, I'm not the only one.  No one has posted what they've done to solve that though.  Any ideas?

  • Anonymous
    January 03, 2010
    Hi rjacobs, Plz chk the link & the changes done by Konkani in crossdomain file  - http://forums.silverlight.net/forums/p/43293/313526.aspx My issue has got solved with this, hope the same for you. Pragati

  • Anonymous
    February 01, 2010
    Great article Brad. One quick question. When decorating the service with RequiresAuthentication, how do you authenticate a client? Thanks, Evan

  • Anonymous
    February 04, 2010
    How is possible to create references in ChangeSetEntries in order to submit a simple hierarchy like Report (1) -> ReportEntry (n), back to server?

  • Anonymous
    February 20, 2010
    @Evan: I didn't find a nice solution for authenticating a client, but the code below works. Basically, you call Login on the AuthenticationService, store the cookie, and send this cookie whenever you need to. AuthenticationServiceClient context = new AuthenticationServiceClient("BasicHttpBinding_AuthenticationService"); using (OperationContextScope operationContext = new OperationContextScope(context.InnerChannel)) {    QueryResult<User> result = context.Login("testuser", "testpassword!", false, null);    // Store cookie somewhere    HttpResponseMessageProperty response = (HttpResponseMessageProperty)OperationContext.Current.IncomingMessageProperties[HttpResponseMessageProperty.Name];    sharedCookie = response.Headers["Set-Cookie"]; } // Later OtherServiceClient context = new OtherServiceClient("BasicHttpBinding_OtherService"); using (OperationContextScope operationContext = new OperationContextScope(context.InnerChannel)) {    // Add stored cookie    HttpRequestMessageProperty request = new HttpRequestMessageProperty();    request.Headers[HttpRequestHeader.Cookie] = sharedCookie;    OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = request;    context.SomeOperationThatRequiresAuthentication(); }