Introduction
If you have not, please see the associated article: POCO Entities Through RIA Service, as this article is the companion to using plain old class objects (POCO) entities through Rich Internet Application (RIA) Services. You could use this example to host anything via the Open Data Protocol (OData), really - not just POCO entities. For this example, you must have the following installed:
Background
A problem arises with using POCOs, which is that you might need to include relationships through LINQ or other data contexts. Microsoft wrote an article on sharing entities using the Include statement, but it does not work with POCOs because they should be context unaware. Also, as is often the case, you might want to join two .edmx files, or provide some highly customized business relationship between your entities, or you might even want to do user access checks before providing data, or you might want to implement the Model View View Model (MVVM) pattern, like this example.
Creating a Domain Service Factory
My WCF RIA Services Class Library project from the previous article (see Introduction) is called EntRiaServices, and it has a sub-project called EntRiaServices.Web. This is where we are going to be creating our Domain Service Classes along with our Domain Service Factory. These are two classes, so right-click on the EntRiaServices.Web sub-project and select Add, New Item, Class.
A Domain Service Factory (DomainServiceFactory.cs) is a class that will produce your available domain services for RIA Services to access:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel.DomainServices.Server;
namespace EntRIAServices.Web
{
public class DomainServiceFactory : IDomainServiceFactory
{
private IDomainServiceFactory _defaultFactory;
private Entities.DBData _dataContext;
public DomainServiceFactory(IDomainServiceFactory defaultFactory)
{
_defaultFactory = defaultFactory;
_dataContext =
new Entities.DBData("metadata=C:\\Users\\Owner\\Documents\\" +
"Visual Studio 2010\\Projects\\BasicDataViewer\\BasicDataViewer\\" +
"bin\\DBData.csdl|C:\\Users\\Owner\\Documents\\Visual Studio 2010\\" +
"Projects\\BasicDataViewer\\BasicDataViewer\\bin\\DBData.ssdl|C:\\" +
"Users\\Owner\\Documents\\Visual Studio 2010\\Projects\\" +
"BasicDataViewer\\BasicDataViewer\\bin\\DBData.msl;provider=" +
"System.Data.SqlClient;provider connection string='Data Source=" +
"YOUR_PC_NAME\\YOUR_SQL_SERVER_NAME;Initial Catalog=YOUR_DB_NAME;" +
"User ID=DB_USER_NAME;Password=YOUR_DB_PASSWORD;" +
"MultipleActiveResultSets=True'");
}
public DomainService CreateDomainService(Type domainServiceType,
DomainServiceContext context)
{
// Here is where you would filter your entities based
// on user information and/or load multiple entities by
// passing in relationship objects
if ((domainServiceType == typeof(AccessableDomainService)))
{
DomainService ds = (DomainService)Activator.CreateInstance(
domainServiceType, new object[] { this._dataContext });
ds.Initialize(context);
return ds;
}
else
{
return _defaultFactory.CreateDomainService(domainServiceType, context);
}
}
public void ReleaseDomainService(DomainService domainService)
{
domainService.Dispose();
}
}
}
The second class (AccessableDomainService.cs), or however many you want, is your accessible Domain Service(s). All of your Domain Services should expose subroutines marked with [Query]
, [Update]
, [Insert]
, and [Delete]
attributes:
using System.Data.Objects;
namespace EntRIAServices.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;
[EnableClientAccess()]
public class AccessableDomainService : DomainService
{
private Entities.DBData _context;
protected string EntitySetName { get; private set; }
public AccessableDomainService(Entities.DBData dataContext)
{
this._context = dataContext;
fFetchEntitySetName();
}
private void fFetchEntitySetName()
{
var entitySetProperty =
this._context.GetType().GetProperties().Single(
p => p.PropertyType.IsGenericType &&
typeof(IQueryable<>).MakeGenericType(
typeof(Entities.Zip)).IsAssignableFrom(p.PropertyType));
this.EntitySetName = "DBData." + entitySetProperty.Name;
}
[Query(IsDefault = true)]
public IQueryable<entities.zip> GetByDallas()
{
return (IQueryable<entities.zip>) this._context.Zips.Where(
x => x.City == "Irving");
}
[Update]
public virtual void SaveCommand(Entities.Zip oZipToSave)
{
object oOriginalItem;
if (this._context.TryGetObjectByKey(new System.Data.EntityKey(
this.EntitySetName, "ZipCode", oZipToSave.ZipCode),
out oOriginalItem))
{
this._context.ApplyCurrentValues(this.EntitySetName, oZipToSave);
}
else
{
this._context.AddObject(this.EntitySetName, oZipToSave);
}
this._context.SaveChanges();
}
[Insert]
public virtual void Add(Entities.Zip oNewZip)
{
this._context.AddObject(this.EntitySetName, oNewZip);
this._context.SaveChanges();
}
[Delete]
public virtual void Delete(Entities.Zip oZip)
{
this._context.DeleteObject(oZip);
this._context.SaveChanges();
}
}
}
As you can see from these two classes, the factory overrides the CreateInstance
of the particular class and passes in the data context. You could pass in anything at this point, including relationship objects. Another thing you could do is if all of your Domain Services for a particular POCO have the same interface, then you could use polymorphism while associating your Silverlight .xaml with a "UI approved" Domain Service from that set.
Connecting Silverlight to Your Domain Service Factory
My SilverLight Application project from the previous article (see Introduction) is called BusinessApplication1 or BusinessApp. I had to drag a Global.asax in from another project, but if you know how to create one, go for it. The domain service factory can be changed from anywhere in your code, but I like it here:
using System;
using System.Web.DynamicData;
using System.Web.Routing;
using System.ServiceModel.DomainServices.Server;
namespace BusinessApp.Web
{
public class Global : System.Web.HttpApplication
{
void Application_Start(object sender, EventArgs e)
{
if (!(DomainService.Factory is
EntRIAServices.Web.DomainServiceFactory))
{
DomainService.Factory =
new EntRIAServices.Web.DomainServiceFactory(DomainService.Factory);
}
}
}
}
Next, you are going to update your .XAML file to include a button. If you were following the previous article, you should already have data on your .xaml and the riaControls:DomainDataSource.DomainContext
reference setup. The data context in my example is named zipDomainDataSource
. I have a demo Save button called Button1
. Here is a quick test for the code-behind to perform an update; you will need to press F5 after entering this into your .xaml.cs for your button:
XAML:
<navigation:Page x:Class="BusinessApp.Page1"
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"
xmlns:navigation="clr-namespace:System.Windows.Controls;
assembly=System.Windows.Controls.Navigation"
d:DesignWidth="640" d:DesignHeight="480"
Title="Page1 Page"
xmlns:riaControls="clr-namespace:System.Windows.Controls;
assembly=System.Windows.Controls.DomainServices"
xmlns:my="clr-namespace:EntRIAServices.Web;assembly=EntRIAServices"
xmlns:my1="clr-namespace:Entities;assembly=EntRIAServices"
xmlns:myApp="clr-namespace:BusinessApp"
xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk">
<riaControls:DomainDataSource AutoLoad="True"
d:DesignData="{d:DesignInstance my1:Zip, CreateList=true}"
Height="0" LoadedData="zipDomainDataSource_LoadedData"
Name="zipDomainDataSource" QueryName="GetZipsQuery" Width="0">
<riaControls:DomainDataSource.DomainContext>
<my:EntityDomainContext />
</riaControls:DomainDataSource.DomainContext>
</riaControls:DomainDataSource>
<UserControl.Resources>
<myApp:ZipViewModel x:Key="zipDomainDataSource"></myApp:ZipViewModel>
</UserControl.Resources>
<Grid x:Name="LayoutRoot">
<Grid DataContext="{Binding ElementName=zipDomainDataSource, Path=Data}"
HorizontalAlignment="Left" Margin="319,0,0,311"
Name="grid1" VerticalAlignment="Bottom">
<Grid HorizontalAlignment="Left"
Margin="114,166,0,0" Name="grid3"
VerticalAlignment="Top">
<Grid.DataContext>
<Binding Source="{StaticResource zipDomainDataSource}" Path="Data" />
</Grid.DataContext>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<sdk:Label Content="Tax Rate:" Grid.Column="0" Grid.Row="0"
HorizontalAlignment="Left" Margin="3"
VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="0" Height="23"
HorizontalAlignment="Left" Margin="3"
Name="taxRateTextBox"
Text="{Binding Path=TaxRate, Mode=TwoWay,
NotifyOnValidationError=true, ValidatesOnExceptions=true}"
VerticalAlignment="Center" Width="120" />
</Grid>
<Button Click="button1_Click" Content="Button"
Height="40" HorizontalAlignment="Left"
Margin="116,262,0,0" Name="button1"
VerticalAlignment="Top" Width="132" >
</Button>
<Button Command="{Binding SaveMe}"
Content="Button" Height="40"
HorizontalAlignment="Left" Margin="116,262,0,0"
Name="button1" VerticalAlignment="Top" Width="132" >
<Button.DataContext>
<Binding Source="{StaticResource zipDomainDataSource}" />
</Button.DataContext>
</Button>
</Grid>
</navigation:Page>
.xaml.cs
private void button1_Click(object sender, RoutedEventArgs e)
{
zipDomainDataSource.SubmitChanges();
}
Abstracting the Interface
This is included in the downloadable project, so if you are after the MVVM pattern using POCOs, here is my View Model and associated classes. These classes are in my Silverlight Application project.
The View Model class provides access to the data, and exposes commands that can be called from the UI using delegates (ZipViewModel.cs):
using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.ComponentModel;
using System.ServiceModel.DomainServices.Client;
using Entities;
namespace BusinessApp
{
public class ZipViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public ICommand SaveMe { get; set; }
private System.Collections.IEnumerable _myData;
private ICollectionView _myDataView;
private DomainDataSource _myDomainDataSource;
public ZipViewModel()
{
this._myDomainDataSource = new DomainDataSource();
this._myDomainDataSource.AutoLoad = true;
this._myDomainDataSource.Name = "zipDomainDataSource";
this._myDomainDataSource.LoadingData +=
new EventHandler<loadingdataeventargs>(_zipDomainDataSource_LoadingData);
this._myDomainDataSource.LoadedData +=
new EventHandler<loadeddataeventargs>(_zipDomainDataSource_LoadedData);
this._myDomainDataSource.DomainContext =
new EntRIAServices.Web.AccessableDomainContext();
this._myDomainDataSource.QueryName = "GetDefaultQuery";
this._myDomainDataSource.Load();
this.SaveMe = new DelegateCommand(SaveCommand, SaveCommand_CanExecute);
this._myData = this._myDomainDataSource.Data;
this._myDataView = this._myDomainDataSource.DataView;
}
void _zipDomainDataSource_LoadingData(object sender, LoadingDataEventArgs e)
{
}
void _zipDomainDataSource_LoadedData(object sender, LoadedDataEventArgs e)
{
}
public System.Collections.IEnumerable Data
{
get { return this._myData; }
set { this._myData = value; }
}
public ICollectionView DataView
{
get { return this._myDataView; }
}
private void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
private void SaveCommand(object parameter) {
this._myDomainDataSource.DomainContext.SubmitChanges();
}
public bool SaveCommand_CanExecute(object parameter)
{
return true;
}
}
}
The View Model class calls this custom handler class to wrap the common command interface:
using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Windows.Input;
namespace BusinessApp
{
public class DelegateCommand : ICommand {
Func<object, bool> canExecute;
Action<object> executeAction;
bool canExecuteCache;
public DelegateCommand(Action<object>
executeAction, Func<object, bool> canExecute)
{
this.executeAction = executeAction;
this.canExecute = canExecute;
}
#region ICommand Members
public bool CanExecute(object parameter)
{
bool temp = canExecute(parameter);
if (canExecuteCache != temp)
{
canExecuteCache = temp;
if (CanExecuteChanged != null)
{
CanExecuteChanged(this, new EventArgs());
}
}
return canExecuteCache;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
executeAction(parameter);
}
#endregion
}
}
Lastly, I want to access the View Model class directly from my .xaml file; you can remove all references to RIA Services and do late binding like this if you want:
namespace BusinessApp
{
public partial class Page1 : Page
{
public Page1()
{
DataContext = new ZipViewModel();
InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
}
}
}
However, it is my preference to reference the View Model directly from the .xaml file because then we can see the object in our data sources. All you have to do is create a reference to your namespace in the page definition: xmlns:myApp="clr-namespace:BusinessApp". Then just reference that resource:
<UserControl.Resources>
<myApp:ZipViewModel x:Key="zipDomainDataSources"></myApp:ZipViewModel>
</UserControl.Resources>
Using the context in a control is done by setting the explicit data context:
<Button.DataContext>
<Bind Source="{StaticResource zipDomainDataSource}" Path="Data" />
</Button.DataContext>
Command callbacks can then be done by calling the iCommand
on your View Model (see the stricken portion from the above .xaml):
Command = "{Binding SaveMe}"
Conclusion
Here is a sample of this solution; you will need to use 7Zip to extract these files because of the high rate of compression:
As you can see, having a domain service factory adds a tremendous amount of flexibility to how domain services are provided, and keeps our POCO DLLs context unaware. Concluding this article, the attached solution demonstrates a Silverlight 4 solution using the Model View View Model (MVVM) pattern and POCO entities. I hope this helps all of you C# business application developers out there, because this technology is still fairly new, and it is hard to find good resources.