A Custom ASP.NET Silverlight 5.0 RIA Services DomainService with Dapper-Dot-Net

Silverlight RIA Services has undergone steady improvements, and it is included With Silverlight 5.0. WCF RIA Services simplifies the traditional n-tier application pattern by bringing together the ASP.NET and Silverlight platforms. RIA Services provides a pattern to write application logic that runs on the mid-tier and controls access to data for queries, changes and custom operations.

It also provides end-to-end support for common tasks such as data validation, authentication and roles by integrating with Silverlight components on the client and ASP.NET on the mid-tier.

It's easy to find RIA Services examples based on Entity Framework or LINQ-To-SQL, but what happens when you want to use a custom data-access framework such as NHibernate, or as in this case, Dapper? You need to know how to create a custom DomainService class. To do this for a custom provider such as Dapper, you need to derive your class from DomainService.

To work with the Silverlight client, a DomainService must be decorated with the EnableClientAccess attribute. You add methods to your domain service that perform the operations you want to define - Query, Update, Insert, Delete, Ignore, and Invoke. When you expose your data in a domain service, it generates an EntitySet object in the domain context that is propagated to the client.

You execute modifications to your data by modifying the entity collection (usually an instance of ObservableCollection) and then call the SubmitChanges method. How the changes actually get processed is entirely up to you - it does not have to be via Entity Framework.

The Invoke operation provides an out-of-band mechanism for returning non-entity data and executing operations. This operation is normally not used for query type methods. For this demo, I'm going to use the ubiquitous Northwind Employees table. First, we need an Employee class that mirrors the columns in the table:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Runtime.Serialization;
using System.Web;

namespace RIA.Web
{
    [DataContract]
     public class Employee
    {
         [Key]
        [DataMember]
        public int EmployeeID { get; set; }

         [DataMember]
        public string LastName { get; set; }

         [DataMember]
        public string FirstName { get; set; }

         [DataMember]
        public string Title { get; set; }

         [DataMember]
        public DateTime BirthDate { get; set; }

         [DataMember]
        public DateTime HireDate { get; set; }

         [DataMember]
        public string Address { get; set; }

         [DataMember]
        public string City { get; set; }

         [DataMember]
        public string Region { get; set; }

         [DataMember]
        public string PostalCode { get; set; }

         [DataMember]
        public string Country { get; set; }

         [DataMember]
        public string HomePhone { get; set; }
    }
}

Note that the class is decorated as a DataContract, and each property is decorated with the DataMember attribute. I do not have all the columns in the Northwind Employees table in this class.

When you create your Silverlight Solution, remember to check the box "Enable RIA Services". I'll be using a DataGrid with a DomainDataSource control, which is preconfigured to be able to work with a DomainService context.

Next, we need to add a DomainService class. For a custom solution like this one, it must derive from the base DomainService class (not the EntityFramework or other variants). Here's the class first:

using System.Collections.ObjectModel;
using System.Runtime.Serialization;
using System.ServiceModel;

namespace RIA.Web
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.ComponentModel.DataAnnotations;
    using System.Linq;
    using System.ServiceModel.DomainServices.Hosting;
    using System.ServiceModel.DomainServices.Server;
    using Dapper;
  
    [EnableClientAccess()]
    public class DomainService1 : DomainService
    {
         public ObservableCollection<Employee> GetEmployeesObservable()
        {
            var result = Dapper.SqlMapperUtil.SqlWithParams<Employee>("Select * from Employees", null, "Northwind");
            var coll = new ObservableCollection<Employee>(result);
             return coll;
        }

   

    public void InsertEmployee(Employee currentEmployee)
    {
        var parms = new
                        {

                            FirstName = currentEmployee.FirstName,
                            LastName = currentEmployee.LastName,
                            City = currentEmployee.City,
                            Region = currentEmployee.Region,
                            Country = currentEmployee.Country,
                            Title = currentEmployee.Title,
                            Address = currentEmployee.Address,
                            BirthDate = currentEmployee.BirthDate,
                            HireDate = currentEmployee.HireDate,
                            PostalCode = currentEmployee.PostalCode,
                            HomePhone = currentEmployee.HomePhone
                        };

            string sql = @"INSERT INTO Employees(Firstname , Lastname, Address,Title,BirthDate,HireDate,Region,City,PostalCode,Country,HomePhone)
                             VALUES(@FirstName, @LastName, @Address, @Title, @BirthDate,@HireDate,@Region, @City, @PostalCode, @Country, @HomePhone)";

          Dapper.SqlMapperUtil.InsertUpdateOrDeleteSql(sql, parms, "Northwind");
    }
  
    public void UpdateEmployee(Employee currentEmployee)
    {
         var parms = new  { EmployeeID = currentEmployee.EmployeeID,
             FirstName=currentEmployee.FirstName ,
             LastName =currentEmployee.LastName ,
             City =currentEmployee.City ,
             Region= currentEmployee.Region ,
             Country = currentEmployee.Country ,
             Title = currentEmployee.Title,
             Address = currentEmployee.Address ,
             BirthDate = currentEmployee.BirthDate ,
             HireDate = currentEmployee.HireDate ,
             PostalCode = currentEmployee.PostalCode ,
             HomePhone = currentEmployee.HomePhone

             };

        Dapper.SqlMapperUtil.InsertUpdateOrDeleteSql("UPDATE Employees set Firstname = @FirstName, Lastname=@LastName, Address=@Address,Title=@Title," +
                                                     "BirthDate=@BirthDate,HireDate=@HireDate,Region=@Region,City=@City,PostalCode=@PostalCode, Country=@Country," +
                                                     " HomePhone=@HomePhone WHERE EmployeeID = @EmployeeID", parms, "Northwind");
    }

  
      
    
       protected override void Dispose(bool disposing)
       {
           //Session.Dispose();
          base.Dispose(disposing);
  
       }
    }
}

Notice that [EnableClientAccess] decorates the class. I have a GetEmployees, and InsertEmployee, and an UpdateEmployee method. Each of these uses the Dapper SqlMapper class, as well as my "Helper" SqlMapperUtil class, which sports methods that make executing various types of queries - either with or without stored procedures - much easier. Finally, it is not necessary to override the ExecuteChangeSet or other base class methods as they work just fine by themselves. All these new public query methods will be exposed automatically to the Silverlight client, which autogenerates a client version of the class on the fly.

On my main Silverlight Page in the client, I have a DataGrid whose ItemsSource is bound to a DomainDataSource control.  I also have a button to call the context's SubmitChanges method, which will automatically process inserts, updates and deletes on the server side.


<UserControl x:Class="RIA.MainPage"
    
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
     
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
     
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
     
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
     
mc:Ignorable="d"
    
d:DesignHeight="399" d:DesignWidth="946" xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk" xmlns:riaControls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.DomainServices" xmlns:my="clr-namespace:RIA.Web">

    <Grid x:Name="LayoutRoot" Background="White">
        
         
         <sdk:DataGrid AutoGenerateColumns="True" Height="248" HorizontalAlignment="Left" Margin="12,12,0,0" Name="dataGrid1" VerticalAlignment="Top" Width="922" ItemsSource="{Binding ElementName=employeeDomainDataSource, Path=Data}" />
        <Button Content="Update" Height="23" HorizontalAlignment="Left" Margin="143,286,0,0" Name="button2" VerticalAlignment="Top" Width="75" Click="button2_Click" />
        <riaControls:DomainDataSource AutoLoad="True" d:DesignData="{d:DesignInstance my:Employee, CreateList=true}" Height="0" LoadedData="employeeDomainDataSource_LoadedData" Name="employeeDomainDataSource" QueryName="GetEmployeesObservableQuery" Width="0"  >
            <riaControls:DomainDataSource.DomainContext>
                 <my:DomainService1 />
            </riaControls:DomainDataSource.DomainContext>
        </riaControls:DomainDataSource>
      
         
    </Grid>
</UserControl>

The codebehind for the MainPage.xaml.cs page follows:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Net;
using System.ServiceModel.DomainServices.Client;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

using RIA.Web;

namespace RIA
{
    public partial class MainPage : UserControl
    {
         private ReadOnlyObservableCollection<Employee> Emps;
         private DomainService1 ctx;
        public MainPage()
        {
             InitializeComponent();
        }

         void Emps_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
             // don't need this with a DomainDataSource
        }

         //update
        private void button2_Click(object sender, RoutedEventArgs e)
         {
             this.employeeDomainDataSource.SubmitChanges();
         }

        private void employeeDomainDataSource_LoadedData(object sender, System.Windows.Controls.LoadedDataEventArgs e)
        {
             if (e.HasError)
            {
                System.Windows.MessageBox.Show(e.Error.ToString(), "Load Error", System.Windows.MessageBoxButton.OK);
                e.MarkErrorAsHandled();
            }
        }
    }
}

All of the above comprises everything I need to enable Silverlight RIA Services with a custom DomainService and my own custom data framework. Of course, this is just a demo. Normally, having the user interface tightly coupled to the service through the DomainDataSource is not a very good practice. For an actual production application, I would refactor this to use MVVM or Laurent Bugnion's MVVM Light with a MainViewModel that derives from INotifyPropertyChanged, and using a CollectionView class which is particularly well suited to work with RIA Services

You can download the demo Visual Studio 2010 Silverlight 5 solution, which includes all classes and a Dapper project containing the SqlMapper and SqlMapperUtils classes.

By Peter Bromberg   Popularity  (11151 Views)