|
|
-
In my previous post about using Silverlight 4 Commanding, I got a question why I didn’t use delegates etc instead of creating a lot of ICommand classes. In this post I’m going to show how you can reduce the number of ICommand classes.
I have created a class with the name Command, this class implements the ICommand interface and uses two constructor arguments of a delegate type, in this case the Predicate<T> and Action<T>. I use the Predicate<T> for the ICommand.CanExecute because it takes one argument and returns a bool (I could have used the Func<T, TResult>), and I use the Action<T> for the ICommand.Execute, Action<T> doesn’t return anything and takes one argument.
Here is the implementation of the Command class:
public class Command : ICommand
{
public event EventHandler CanExecuteChanged;
Predicate<Object> _canExecute = null;
Action<Object> _executeAction = null;
public Command(Predicate<Object> canExecute, Action<object> executeAction)
{
_canExecute = canExecute;
_executeAction = executeAction;
}
public bool CanExecute(object parameter)
{
if (_canExecute != null)
return _canExecute(parameter);
return true;
}
public void Execute(object parameter)
{
if (_executeAction != null)
_executeAction(parameter);
if (CanExecuteChanged != null)
CanExecuteChanged(this, new EventArgs());
}
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Because the ICommand’s CanExecute and Execute method takes one argument of type ojbect (the value comes from the ButtonBase class’s CommandParamter) I decided to still use it, so the T in Preditcate<T> and Actiton<T> is of type object. In this example I also added the code to trigger the CanExecuteChanged event after the Execute is done. The reason I did that was because the CanExecute can be based on the result of the Execute. You can trigger the CanExecuteChanged event when you want the UI to check the CanExecute method. The CanExecute will normally take place when the UI is loaded, and before the Execute method is executed.
To specify a Command in a ViewModel with the Command class I only need to return an instance of the Command where I pass two lambda expressions to the constructor. Here is an example:
public class CommandingViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _myTextBox;
private Command _saveCommand = null;
public string MyTextBox
{
get { return _myTextBox; }
set
{
if (_myTextBox != value)
{
_myTextBox = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("MyTextBox"));
}
}
}
public ICommand SaveCommand
{
get
{ if(_saveCommand == null)
_saveCommand = new Command
(
p => string.IsNullOrEmpty(MyTextBox),
p => MyTextBox = "Save Button Pressed"
);
return _saveCommand;
}
}
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
The first lambda expression passed to the first argument of the Command class will return true or false to enable or disable for example a Button where the Command is used. The second lambda expression is the code that will be executed when the Button is pressed (when the Command is executed). The “p” in the expressions will hold the CommandParameter result. Here is the View I used for the example above:
<UserControl
xmlns:view="clr-namespace:Commanding.ViewModel"
...>
<UserControl.Resources>
<view:CommandingViewModel x:Name="myView"/>
</UserControl.Resources>
<Grid x:Name="LayoutRoot" DataContext="{StaticResource myView}">
<StackPanel VerticalAlignment="Center">
<TextBox Height="50" Width="200" Text="{Binding MyTextBox}"/>
<Button Content="Save" Height="50" Width="100" CommandParameter="1" Command="{Binding SaveCommand}"/>
</StackPanel>
</Grid>
</UserControl>
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
As you can see, with a solution where we pass a lambda expression, we can reduce the number classes used and also make sure the Command code stays within the ViewModel.
If you want to know when I publish new blog posts, then you can follow me on twitter: http://www.twitter.com/fredrikn
|
-
Note: This post is based on the WCF RIA Services VS 2008 PDC Beta and VS 2010 Beta 2 preview, changes can be done before the framework hits RTM.
I have seen questions about how to dynamically create search criteria when using WCF RIA Services and LINQ. So this post is about how you can dynamically create a criteria as a string on the client-side and pass it down to the server for execution.
There is a simple way to create a criteria on the client-side by using WCF RIA Services and LINQ and pass it down to the server for execution, here is an example:
var dx = new CustomerDomainService();
EntityQuery<Customer> query = from customer in dx.GetCustomersQuery()
where customer.Country == "Sweden"
select customer;
dx.Load<Customer>(dx.GetCustomersQuery());
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
The problem with the above code and LINQ is that LINQ is static. So if we need to dynamically create a criteria it will be hard.
If we for example have an advanced search form with several ComboBox(s) and also TextBox(s) etc. we want to create a query based on the user’s inputs. It will require a lot of code if we use LINQ and probably an ugly condition statement. Wouldn’t it be great if we could do something like this:
var dx = new CustomerDomainService();
string criteria = string.Format("Country = {0} and CompanyName = {1}", countryComboBox.SelectedItem, companyNameTextBox.Text);
dx.Load<Customer>(dx.GetCustomersQuery(criteria));
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
The above code will dynamically create a criteria based on the user’s input and pass it to the GetCustomersQuery.
The CustomerDomainService GetCustomers Query method will look something like this: [EnableClientAccess()]
public class DomainService1 : LinqToEntitiesDomainService<NORTHWNDEntities>
{
public IQueryable<Customer> GetCustomers(string criteria)
{
return this.ObjectContext.CustomerSet;
}
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
As you may see the criteria isn’t used in the code, and if we want to use it we need to parse the criteria and that will require a lot of code to create different static LINQ queries based on criteria. But there is a solution, that is the point of this blog post ;)
We can use the LINQ Dynamic Query Library to solve the problem. To use the LINQ Dynamic Query Library we need to add “using System.Linq.Dynamic” namespace to our DomainService (this after we have the LINQ Dynamic Query Library). To get it you can simply downloaded the example Lib from one of the following links:
- VB Dynamic Query Library (included in the \Language Samples\LINQ Samples\DynamicQuery directory)
- C# Dynamic Query Library (included in the \LinqSamples\DynamicQuery directory)
What I did to make it work was to add the Dynamic.cs (.vb) class from the example to my Web project. Then added the “using System.Linq.Dynamic” to get the extension method added to the Dynamic.cs (.vb) file to the IEnumerable<T> interface.
When this is done we will have an extended version of the Where extension method which will take a criteria as a string and parse it for us to a valid query.
Here is a simple example where I have created a criteria on the client-side as a string and pass it down to the DomainService, where I use the LINQ Dynamic Query Library:
CustomerDomainService.cs
[EnableClientAccess()]
public class CustomerDomainService : LinqToEntitiesDomainService<NORTHWNDEntities>
{
public IQueryable<Customer> GetCustomers(string criteria)
{
if (!string.IsNullOrEmpty(criteria))
return this.ObjectContext.CustomerSet.Where(criteria, new object[] { });
return this.ObjectContext.CustomerSet;
}
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
MainPage.xaml.cs
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
var dx = new CustomerDomainService();
string criteria = "Country = \"Sweden\" and City <> \"Luleå\"";
dx.Load<Customer>(dx.GetCustomersQuery(criteria));
customersDataGrid.ItemsSource = dx.Customers;
}
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
When using the LINQ Dynamic Query Library all strings need to be enclosed by “ “, so that is the reason why you see \”Sweden\”. To make the criteria more interesting I also added that the City property shouldn’t be “Luleå” (Luleå is a city far to the north in Sweden).
The LINQ Dynamic Query Library will also work on Linq To SQL etc.
Summary
By using the LINQ Dynamic Query Library we can now in a easier way dynamically create a criteria based on users options, for example on a advanced search form.
If you want to know when I post a new blog post on my blog etc, you can follow me on twitter: http://www.twitter.com/fredrikn
|
-
Note: This blog post will use the WCF RIA Services PDC Beta and VS2010 Preview, changes to the framework can me made before it hits RTM. Some questions that pops up about WCF RIA Services is how to enable transactions. So I decided to write a blog post about it, but to not make this post just a few lines I also decided to write about the DomainService life-cycle so you will know what will happen step-by-step in the DomainService after a DomainContext’s SubmitChanges is called on the client-side. The following images will show you the part of the DomainService life-cycle when a SubmitChanges takes place: The first thing that will happen is the creation of the DomainService, this is done through a Domain Service Factory, you can read about it here. The Domain Service Factory will make a call to the DomainService Initialize method. Within this method you can for example set the ConnectionString on the ObjectContext’s Connection if you are using the LintToEntitesDomainService, or the DataContext if you are using the LintToSqlDomainService. Here is an example of changing the ObjectContext’s Connection’s ConnectionString:
[EnableClientAccess()]
public class CustomerDomainService : LinqToEntitiesDomainService<NORTHWNDEntities>
{
public override void Initialize(DomainServiceContext context)
{
this.ObjectContext.Connection.ConnectionString = "";
base.Initialize(context);
}
...
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
After the Initialization the Submit method of the DomainService will be executed. This method will execute the following DomainService methods in the following order:
1) AuthorizationChangeSet
2) ValidateChangeSet
3) ExecuteChangeSet
4) PersistChangeSet
5) ResolveChangeSet
Note: I will only focus on the virtual method which you can overwrite.
You can override the Submit method and do something before the Submit will take place, and also do things after the Submit is executed, for example do some pre- and post conditions like logging etc.
Note: If you want to log exceptions that will take place during a Submit, it’s recommended that you override the OnError method and log the exception within that method.
public override bool Submit(ChangeSet changeSet)
{
//do something here pre submit
var submitResult = base.Submit(changeSet);
//do something here post submit
return submitResult;
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
The base.Submit will make a call to the OnError if there was any error during the process of the Submit method, so it will not take place after the Submit method. There is a try and catch block around all the above mentioned methods within the base.Submit method so if anyone of them will fail the Submit method will make a call to the OnError an then throw the exception up the chain.
Note: The PersistChangeSet methods of the LinqToEntitiesDomainService and LinqToSqlDomainService can also make a call to the OnError method if a concurrency conflict occurs during the persistent of the entities.
As I mentioned earlier the Submit method will make a call to some methods in a specific order, the first method is the AuthorizeChangeSet, but before taking a look at the AuthorizeChangeSet method I will focus on the ChangeSet object, which every xxxxChangeSet methods will take as an argument.
The ChangeSet object
The ChangeSet object is like a Unit of Work (things that should be done in a specific order). The ChangeSet will contains a list of Domain Operations that should take place and also the entity which is part of that operation, for example if you do the following operation on the client-side:
updateCustomer.Name = "Jane Doe";
var newCusotmerc = new Customer() { CustomerID = 2, Name = "Fred Doe" };
domainContext.Customers.Add(newCustomer);
domainContext.Customers.Remove(removeCustomer);
domainContext.SubmitChanges();
The ChangeSet’s ChangeSetEntries will have three ChangeSetEntry:
Operation = Insert, Entity = newCustomer
Operation = Update, Entity = updateCustomer
Operation = Delete, Entity = removeCustomer
The Operation property of the ChangeSetEntry has the name of the operation that should take place. A operation method is the method which you add to the DomainService to Insert, Update or Delete an entity. The method use a naming convention where the prefix of the method name is the name of the operation, the method can only take an Entity as argument, here is an example of three operation methods added to the DomainService:
public void InsertCustomer(Customer customer)
public void UpdateCustomer(Customer customer)
public void DeleteCustomer(Customer customer)
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
The following name can be used for the Insert operation: Insert, Create and Add, or the InsertAttribute on the method if you don’t want to use the naming convention.
The following name can be used for the Update operation: Update. Modify and Edit, or the UpdateAttribute on the method.
The following for the Delete operation: Delete and Remove or the DeleteAttribute.
There is something called a “custom” method. A Custom method can’t return anything and the first argument must be an entity the rest a valid RIA Services type. The name of the method doesn’t matter. Here is an example of a Rebate custom method:
public void Rebate(Customer customer)
When a “custom” method is called on the client-side, it will not be added to the ChangeSet as a Operation will. Instead it will be added as an EntityAction to the ChangeSet’s Entity.
The Entity property holds the Entity that should be passed to the specific Operation and also the “custom” methods that should be executed.
I will not dig any deeper into the ChangeSet class, it has more properties and method for Entity associations and error etc.
Now to the different xxxxxChangeSet methods.
AuthorizeChangeSet
The AuthorizeChangeSet method is the first method that will be executed when the Submit method is called. This method will validate and see if the current user are allowed to make a call to the DomainService operations. The AuthorizeChangeSet method will iterate through the ChageSet and see if any of the Operations in ChangeSet or the “custom” methods has any of the the AuthorizeAttribute specified (ReuqiredAuthenticationAttribute or RequiredRoleAttrtibute). The AuthorizeAttribute is used to prevent non-authorized user to make a call to an operation method. If the current user isn’t allowed to execute any of the operations in the ChangeSet an exception will be thrown and the OnError method of the DomainService will be called, and the execution of the Submit will ended. Here is an example where the AuthroizateAttribute is used:
[RequiresAuthentication(), RequiresRole("Admin")]
public void AddCustomer(CustomerDto customer)
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
If the current user isn’t authenticated or the authenticated user don’t belong to the role “Admin”, the AuthorizeChangeSet will fail and an exception is thrown.
ValidateChangeSet
The next method that will be executed if the authorize validation passed during the Submit is the ValdiateChangeSet. This method will perform the server-side validation when the validation annotation is used on entities. If the validation fails on a entity in the ChangeSet the OnError method will be called and the Submit method will break. The following is an example about how to use the validation annotation on a Entity Framework or Linq to SQL generated class:
[MetadataTypeAttribute(typeof(Customer.CustomerMetadata))]
public partial class Customer
{
internal sealed class CustomerMetadata
{
private CustomerMetadata()
{
}
[Required]
[RegularExpression("[A-Z][A-Za-z0-9]*")]
[StringLength(32)]
public string CompanyName;
...
}
} }
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
If you use a DTO/”Presentation Model” no meta data types are needed, instead you can just add the validation attributes to the property of your DTO/”Presentation Model”:
public class Customer
{
[Required]
[RegularExpression("[A-Z][A-Za-z0-9]*")]
[StringLength(32)]
public string CompanyName { get; set;}
...
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Note: The validation will take place on the client-side, so the WCF RIA Services validation annotation will do both a client-side validation and server-side. It’s the ValidateChangeSet method that will do the validation on the server-side.
ExecuteChangeSet
The ExecuteChangeSet will take place after the validation is passed. This method will make two internal execution, it will first iterate through the ChangeSet and execute the CUD (Create, Update and Delete) Operations, and then the “custom” methods will be executed if there are any in the ChangeSet. If the ChangeSet has the following ChangeSetEntries:
Operation = Insert, Entity = newCustomer
Operation = Update, Entity = updateCustomer
Operation = Delete, Entity = removeCustomer
and the following operation methods in the DomainService:
public void InsertCustomer(Customer customer)
public void UpdateCustomer(Customer customer)
public void DeleteCustomer(Customer customer)
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
The first method to be executed during the ExecuteChangeSet will be the InsertCustomer then the UpdateCustomer and last the DeleteCustomer. When the ExecuteChangeSet method is completed the PersitChangeSet method will be executed.
PersistChangeSet
The PersistChangeSet method will make sure to save the inserted, updated and deleted entities to a data source. If the LintToEntityDomainService is used the PersistChangeSet will make a call to the ObjectContext’s SaveChanges method, if LinqToSqlDomainService is used the PersistChangeSet will call the DataContext’s SubmitChanges method. If a normal DomainService is used (when you use DTO/”Presentation Model”), the PresistChangeSet will do nothing if you don’t implement it. If you for example uses nHibernate you can call the nHibernate’s Session’s Save method in the PersistChangeSet, if the other operation methods just add entities to or remove entities from the nHibernate’s session. If you simply use the Repository pattern, Business Logic Components or Data Access components, you don’t need to use the PersistChangeSet, instead the operations method will work against the components instead (of course it’s up to you and based on the way you want to persist the model).
The last method that will be executed by the Submit method is the ResolveChangeSet, it will only be executed if the ChangeSet’s Entities got any conflicts during the process of the Submit method.
ResolveChangeSet
The ResolveChangeSet will try to resolve any conflicts appeared during the execution of the Submit method. If the conflicts are solved the method will return true otherwise false. If LinqToEntityDomainService or the LinqToSqlEntityDomainService is used, the ResolveChangeSet will make a call to the ObjectContext’s SaveChanges or the DataContext’s SubmitChanges after it succeeded to resolve the entities.
Now when you have got the basic understanding about what the Submit method in the DomainService does, it’s time to see where to begin and end a transaction.
Using Transactions within the DomainService
I have seen some examples where people starts an transaction within the Submit method. A transaction should be short lived and by starting a Transaction within the Submit method like this:
public override bool Submit(ChangeSet changeSet)
{
using (var trans = new TransactionScope())
{
var result = base.Submit(changeSet); trans.Complete();
return result;
}
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
can make sure the transaction will be up an running during the time the Submit method will call the AuthorizationChangeSet, ValidateChangeSet, ExecuteCHangeSet and the PersistChangeSet. It can take time for the methods to be executed and there is basically no reason to have a open transaction during the execution of some of the methods. The only methods that may need the Transaction is the ExecuteChangeSet or the PersistChangeSet and maybe the ResolveChangeSet method if you use the LinqToEntityDomainService or the LinqToSqlDomainServices. The LinqToEntityDomainService and the LinqToSqlDomainServices will try to Save- or SubmitChanges after it tries to resolve some of the conflict during the ResolveChangeSet method.
The reason why you may want to being the transaction within the Submit method it to make sure the ResolveChangeSet will be using the same transaction as the PersistChangeSet. Another solution to avoid having an transaction open before the PersistChangeSet is called, is by starting a transaction within the PersistChangeSet and Commit or rollback the exception within the Submit method, then the ResolceChangeSet will also reuse the same transaction:
private TransactionScope _trans = null;
protected override bool PersistChangeSet(ChangeSet changeSet)
{
if (_trans == null ) _trans = new TransactionScope();
return base.PersistChangeSet(changeSet);
}
public override bool Submit(ChangeSet changeSet)
{
bool submitResult = false;
try
{
var submitResult = base.Submit(changeSet);
if (trans != null)
trans.Complete();
}
catch
{
if (trans != null)
{
trans.Dispose();
trans = null;
}
}
return submitResult;
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Instead of using the TransactionScope, you could also begin an transaction by using the LinqToEntitesDomainService ObjectContext’s Connection property's BeginTransaction method or the LinqToSqlDomainService DataContext’s Connetion property’s BeginTransaction method. But in that case you will not use a distributed transaction, only a local database connection. If you are using DTO/”Presentation Model” and your operation methods are working directly against your Business layer from the DomainService, you can start the transaction within the ExecuteChangeSet method.
Summary
You have now seen the life-cycle of the DomainService and what methods it will execute during a Submit. You have also seen where you can start a transaction.
If you want to know when I post a new blog post you can follow me on twitter: http://www.twitter.com/fredrikn
|
-
Note: The WCF RIA Services examples in this post is based on the WCF RIA Services PDC Beta for VS 2008 and VS 2010 Beta 2 Preview.
I have notice that a lot of developers are passing a lot of data over the wire, like they have forgot or don’t see the network between the client and the server. In this post I’m going to show how some solutions can be used to minimize the loading of data. I’m not going to mention about using techniques like GZIP compression with IIS to reduce the size, instead how to use the DataPager and also do aync. calls to load data on demand etc.
The following example uses WCF RIA Services and the well known Northwind database. The example also uses the LinqToEntitesDomainService to just passing DAL types directly to the client over the network, but all the examples will work fine even with DTO/”Presentation Model”.
Note: By using DTO instead or create a Entity Data Model for presentation purpose only,you can reduce the number of values passed to the client and also focus on the data that should be displayed by the current View, so no extra data is passed. Passing too much data that shouldn’t be displayed on the view can affect performance badly if the app has many users.
Here is the code of the DomainService:
[EnableClientAccess()]
public class CustomerDomainService : LinqToEntitiesDomainService<NORTHWNDEntities>
{
public IQueryable<Customer> GetCustomerSet()
{
return this.ObjectContext.CustomerSet;
}
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
It simply returns all the Customers located in the Northwind’s Customers table. The View has a DataGrid to display all the Customers and here is the XAML for the View:
<UserControl
xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
xmlns:controlsToolkit="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Toolkit" ...>
<Grid x:Name="LayoutRoot">
<data:DataGrid x:Name="customersDataGrid"/>
<controlsToolkit:BusyIndicator x:Name="busyIndicator"/>
</Grid>
</UserControl>
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Note: It’s a good practice to add some kind of ProgressBar while loading data, the reason is because all communications to the Server services are made asynchronous and we can’t predict how long time it will take to load the data.
In the example above the Silverlight 3 November 09 Toolkit’s BusyIndicator is used as an activity indicator.
This example don’t use the WCF RIA Services DomainDataSource control, instead code-behind to load the Customers, the DomainDataSource will be mentioned later in this post.
Here is the code-behind of the View:
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
this.Loaded += new RoutedEventHandler(MainPage_Loaded);
}
void MainPage_Loaded(object sender, RoutedEventArgs e)
{
var customerDomainContext = new CustomerDomainContext();
busyIndicator.IsBusy = true;
customerDomainContext.Load<Customer>(
customerDomainContext.GetCustomerSetQuery(),
lo => busyIndicator.IsBusy = false
null);
customersDataGrid.ItemsSource = customerDomainContext.Customers;
}
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
The code above will show the BusyIndicator before the Loading of Customers takes place, and will hide the indicator when the Customers is returned from the server by passing a callback to the Load method of the customerDomainContext. The customerDataGrid’s ItemsSource property is set to the customerDomainContext’s Customers property. A very simple example of binding a DataGrid with data, but there are several problems with the above code.
1) The DataGrid in Silverlight 3 can take some time to add all rows and update the UI. (As far as I know the DataGrid in Silverlight 3 will not asynchronous add the rows. In a future version of Silverlight the rows may be added asynchronous, haven’t yet confirmed if the DataGird in Silverlight 4 will do it).
2) There are a lot of data passed over the wire because all Customers and the properties of a Customer entity will be passed over the network from the server to the client. If this is a public application we can’t know what kind of bandwidth the different users have etc. Passing too much data can affect performance badly.
3) The users will probably never walk through all the Customers in the list, so what’s the reason to add them all to the DataGrid from start?
The following part of the post will take each three issues above and show different solutions to reduce the above problems.
How to solve the problem where the DataGrid will take some time to display all rows?
One simple way to make sure that not all rows will be added to the DataGrid is by using paging. With Silverlight 3 there is a DataPager control which can be used for paging. The following shows how a DataPager can be added to XAML and be used with a DataGrid:
<UserControl
xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
xmlns:controlsToolkit="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Toolkit" ...>
<Grid x:Name="LayoutRoot">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<data:DataGrid x:Name="customersDataGrid"/>
<data:DataPager
Grid.Row="1"
PageSize="20"
Source="{Binding ItemsSource, ElementName=customersDataGrid}"
IsTotalItemCountFixed="True"
VerticalAlignment="Top"/>
<controlsToolkit:BusyIndicator
Grid.RowSpan="2"
x:Name="busyIndicator"/>
</Grid>
</UserControl>
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
The DataPager will only work if its Source property is set to a PagedCollectionView, so the Customers added to the customersDataGrid ItemsSource property needs to be converted to a PagedCollectionView, this can be done by creating an instance of the PagedCollectionView and pass the Customers as an argument to the PageCollectionView’s constructor.
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
this.Loaded += new RoutedEventHandler(MainPage_Loaded);
}
void MainPage_Loaded(object sender, RoutedEventArgs e)
{
var customerDomainContext = new CusotmerDomainContext();
busyIndicator.IsBusy = true;
customerDomainContext.Load<Customer>(
customerDomainContext.GetCustomerSetQuery(),
lo => busyIndicator.IsBusy = false,
null);
customersDataGrid.ItemsSource = new PagedCollectionView(customerDomainContext.Customers); }
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
The above code will only make sure the DataGrid will show data a lot faster by not adding all rows at once to the DataGrid, but it will still pass all Customers to the client. Some users may not want the paging support, instead they want to have all the Customers listed in the DataGrid. I try to make them understand what kind of problems that can lead to and try to suggest some other solutions, for example by adding a filter to make sure to only list about 500 items, and add a TextBlock to the View with a text like this: “Total number of Customers is 500 out of 25 000 ….”, then make sure they use some kind of filter to load some specific Customers, for example maybe filter on Country and City etc, everything to reduce the number of data passed from the server to the client. With WCF RIA Services there is a DomainDataSource Control which can be used with the DataPager and can asynchronous load the Customers the current page is showing. No code-behind is needed when the DomainDataSource is used. The following is an example where the DomainDataSource is used with a DataPager: <UserControl
xmlns:riaControls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Ria"
xmlns:riaData="clr-namespace:System.Windows.Data;assembly=System.Windows.Controls.Ria"
xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
xmlns:myData="clr-namespace:SilverlightApplication53.Web"
...>
<Grid x:Name="LayoutRoot">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<data:DataGrid
x:Name="customersDataGrid"
ItemsSource="{Binding Data, ElementName=dds}"/>
<data:DataPager
Grid.Row="1"
PageSize="20"
Source="{Binding Data, ElementName=dds}"
IsTotalItemCountFixed="True"
VerticalAlignment="Top"/>
<riaControls:DomainDataSource
x:Name="dds"
AutoLoad="True"
PageSize="20"
LoadSize="30"
QueryName="GetCustomerSet">
<riaControls:DomainDataSource.DomainContext>
<myData:DomainService1/>
</riaControls:DomainDataSource.DomainContext>
<riaControls:DomainDataSource.SortDescriptors>
<riaData:SortDescriptor PropertyPath="CompanyName" />
</riaControls:DomainDataSource.SortDescriptors>
</riaControls:DomainDataSource>
</Grid>
</UserControl>
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
By using the DomainDataSource LoadSize property, we can specify how many Customers that should be loaded at a time. The PageSize is used to specify the size of the Page, in this case the each page should display 20 Customers.
The solution by using the DomainDataSource control will solve the three problems mentioned earlier in this post. It will reduce the number of data passed to the client, it will also make sure the DataGrid will show the data much faster and if the users don’t need to see all Customers, they don’t need get all Customers loaded and added to the DataGrid. The only limitation is the use of a DataPager which some users may not want to use.
Another solution to minimize the data passed to the client can be to only get 20 Customers and add them to the DataGird, and when the user moves the DataGrid Scrollbar an asynchronous call is made to get the next 20 Customers and add them dynamically to the DataGrid. By doing so the DataPager isn’t needed and the DataGrid can show the Customers a lot faster, it will also reduce the number of Customers passed from the server to the client.
How to minimize the number of items passed over the wire?
The DomainContext’s Load method takes a Query as an argument, the Query will be passed to the DomainService and will be executed on the server-side. So by using this Query feature we can make sure to filter the data and only make sure the Query method will give us the items we are interested in. The following example will create a Query and pass it down to the server for execution, the query will make sure to Skip a number of Customers and only take 21 Customers:
EntityQuery<Customer> query = _customerDomainContext.GetCustomerSetQuery()
.OrderBy(customer => customer.CompanyName)
.Skip(0)
.Take(21);
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
_customerDomainContext.Load<Customer>(
query,
lo =>
{
busyIndicator.IsBusy = false;
},
null);
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
To reduce the number of items passed over the wire and skip the use of a DataPager we can create our own Custom DataGrid where we will only show the 21 first Customers and when the user moves the DataGrid’s Vertical Scroller we load the next 21 Customers asynchronous and add them to the DataGrid. The reason why we need to create our own custom DataGrid is because we will not have access to the Vertical Scroller from the DataGrid itself. The DataGrid will only add a Scroller if some items will not fit into the height of the DataGrid. If we only get 21 Customers and bind it to the DataGrid and they all will fit into the DataGrid no scroller will be visible, and the users will think they got all Customers. What we need to do is to add a Scroller as if all the Customers are already added to the DataGrid. To make sure to make such as Scroller we need to get the actual height of a DataGrid row and also get the total number of Customers the DataGrid should normally display, and take the height times the number of Customers to fake the Scroller. By using the WCF RIA Services service operation we can do a really fast call to get the total number of Customers, then we can create a Query to load a the 21 first Customers. You may wonder why 21 why not 20, the reason is that we will set the height of the DataGrid so only 20 items will be listed, but to get access to the Scroller and set a its range we need to make it appear. So a simple hack to get access to the Scroller is to just show 20 items but add 21 to the DataGrid. The following code is the new CustomerDomainService with a Service operation to get the total number of Customers:
[EnableClientAccess()]
public class CusotmerDomainService : LinqToEntitiesDomainService<NORTHWNDEntities>
{
public IQueryable<Customer> GetCustomerSet()
{
return this.ObjectContext.CustomerSet;
}
[Invoke]
public int NumberOfCustomers()
{
return this.ObjectContext.CustomerSet.Count();
}
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
The following code is the code-behind of the View that will show the Customers:
public partial class MainPage : UserControl
{
CustomerDomainService _customerDomainContext = new CustomerDomainService();
int _currentPage = 0;
const int NumberOfItemsToLoad = 20;
public MainPage()
{
InitializeComponent();
this.Loaded += new RoutedEventHandler(MainPage_Loaded);
}
void MainPage_Loaded(object sender, RoutedEventArgs e)
{
customersDataGrid.ItemsSource = _customerDomainContext.Customers;
LoadCustomers();
}
private void LoadCustomers()
{
busyIndicator.IsBusy = true;
EntityQuery<Customer> query = _customerDomainContext.GetCustomerSetQuery()
.OrderBy(customer => customer.CompanyName)
.Skip(_currentPage * NumberOfItemsToLoad)
.Take(NumberOfItemsToLoad+1);
_customerDomainContext.NumberOfCustomers(
io =>
{
customersDataGrid.TotalNumberOfRows = io.Value;
_customerDomainContext.Load<Customer>(
query,
lo =>
{
busyIndicator.IsBusy = false;
},
null);
},
null);
}
private void customersDataGrid_LoadNewItems(object sender,
System.Windows.Controls.Primitives.ScrollEventArgs e)
{
_currentPage++;
LoadCustomers();
}
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
I will not focus so much on this code, I assume you can figure out what it does. The NumberOfCustomers Service operation will be executed before loading the Customers to make sure the custom DataGrid’s TotalNumberOfRows property is set before the Customers is added to the DataGird’s ItemsSource. You can also see in the code that the Skip and Take is used to create a Query, the _currentPage will be increased by 1 when the custom DataGrid will trigger its LoadNewItems event handler (The LoadNewItems event will be trigged when the Scroller of the DataGrid reach a specific value, in this case when 20 rows are displayed in the DataGrid and the next 20 must be loaded and added to the DataGrid). The LoadNewItems event handler will also make a call to the LoadCustomers method to Load the next 21 Customers. The following code is the XAML of the View where the custom DataGrid is used:
<UserControl
xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
xmlns:controlsToolkit="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Toolkit"
xmlns:myData="clr-namespace:SilverlightApplicationSample"
...>
<Grid x:Name="LayoutRoot">
<myData:AsyncDataGrid
x:Name="customersDataGrid"
Height="505"
LoadNewItems="customersDataGrid_LoadNewItems"
/>
<controlsToolkit:BusyIndicator
Grid.RowSpan="2"
x:Name="busyIndicator"/>
</Grid>
</UserControl>
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
The following code is the custom DataGrid:
public class AsyncDataGrid : DataGrid
{
public event ScrollEventHandler LoadNewItems;
private double _rowHeight = 0.0;
private double _oldScrollValue = 0.0;
private ScrollBar _verticalScrollBar = null;
public AsyncDataGrid()
{
this.LoadingRow += AsyncDataGrid_LoadingRow;
}
public int TotalNumberOfRows { get; set; }
private double GetNextLoadingPosition()
{
var numberOfItems = 0;
foreach (var item in ItemsSource)
numberOfItems++;
return _rowHeight * numberOfItems;
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_verticalScrollBar = this.GetTemplateChild("VerticalScrollbar") as ScrollBar;
if (_verticalScrollBar != null)
_verticalScrollBar.Scroll +=scrollBar_Scroll;
}
private void AsyncDataGrid_LoadingRow(object sender, DataGridRowEventArgs e)
{
e.Row.SizeChanged += Row_SizeChanged;
this.LoadingRow -= AsyncDataGrid_LoadingRow;
}
private void Row_SizeChanged(object sender, SizeChangedEventArgs e)
{
_rowHeight = e.NewSize.Height;
((DataGridRow)sender).SizeChanged -= Row_SizeChanged;
if (_verticalScrollBar != null)
{
_verticalScrollBar.Maximum = TotalNumberOfRows * _rowHeight;
_verticalScrollBar.UpdateLayout();
}
} private void scrollBar_Scroll(object sender, ScrollEventArgs e)
{
if (LoadNewItems != null && e.NewValue > _oldScrollValue)
{
_verticalScrollBar.Maximum = TotalNumberOfRows * _rowHeight;
if (e.NewValue >= GetNextLoadingPosition())
{
_oldScrollValue = e.NewValue;
LoadNewItems(this, e);
}
}
}
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
By override the OnApplyTemplate method, we can get access to the DataGrid’s Default ControlTemplate’s VerticalScrollbar and hook up to the Scroller’s Scoll event. By using the LoadingRow event and then the SizeChanged on a DataGridRow we can get the height of the DataGridRow (I couldn’t figure out a simple way of doing it, because the LoadingRow event can’t be used to get the ActaulHeight of the row loaded, it’s to early and there is no Rows property to get the UIElement of the DataGrid’s Row, the only way to get a DataGridRow is by using the LoadingRow event). Within the SizeChanged event handler of the DataGridRow, the Maximum property of the Scroller is set to the TotalNumberOfRows times the height of the row, this will make sure the Scroller has a range that will make sure it looks like several of rows are added but they aren’t. Withing the Scoller’s Scroll event handler (scrollBar_Scroll) the LoadNewItems event will be trigged only if the Scroller reach a position when new Customers should be loaded.
Note: I haven’t take care of the key board events, if a user uses the keyboard to walk through all the Customers added and reach the 21 Customer, no Customers will be loaded, but it’s easy to add. I left it out in this example because the hard part is to figure out how to handle the Scroller.
This solution will solve the three problems mentioned earlier in this post. It will reduce the number of data passed to the client, it will also make sure the DataGrid will show the data much faster and if the users don’t need to see all Customers, they don’t need get all Customers loaded and added to the DataGrid.
Summary
This post covered some solutions to minimize the data loaded form the server to the client, and also speed up the DataGrid. This was done by using the DataPager control, or the DomainDataSoure or to load Customers while the users moves a DataGrid’s Scrollbar.
If you want to know when I post a new blog post or just follow my me, you can follow me on twitter: http://www.twitter.com/fredrikn
|
-
Note: Some examples in this post uses the WCF RIA Services PDC Beta, so changes can be done until it hit RTM.
I have already mention about how to handle exceptions with WCF RIA Services here: http://weblogs.asp.net/fredriknormen/archive/2009/12/08/wcf-ria-services-exception-handling.aspx, but this post is about different ways to handle exception occurred on the client-side and how to log those exceptions.
Logging Services
If you want to log exceptions occurred on the client-side to the server-side you need to add a Service which can take the exception message and store it. Silverlight supports different ways of passing data to the server for example, Web Service, WCF Service, Service operation via WCF RIA Services or just passing data to a ASP.NET Web Form etc. In this post I will focus on using WCF RIA Services and also WCF Service, only to give you two different options for using a service to log information.
The following is an example of a WCF Service and a WCF RIA DomainService to act as a logger:
Silverlight enabled WCF Service:
[ServiceContract(Namespace = "")]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class LogService
{
[OperationContract]
public void Log(string message)
{
//Log4Net, Exception or Instrumetionation Application Block (EntLib), EventLog or what you use ...
}
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
WCF RIA Services DomainService:
[EnableClientAccess()]
public class LogDomainService : DomainService
{
[Invoke]
public void Log(string message)
{
//...
}
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Note: I haven’t added code to log the message, I assume you have your own solutions for logging. I also just take one argument of type string which will contain a formatted exception message, if you prefer to use a specific class as an argument with properties to hold more information about the exception, you can do that. I will only keep this example simple for demonstrate the concept.
The InvokeAttribute added to the DomainService will make the method as a Service operation, a method which we can simply call without using the WCF RIA Services DomainContext’s SubmitChanges.
Handling Exception on the client-side
If you want to use a generic way to handle exceptions on the client-side and call the logging service, you can use the Application_UnhandledException located in the code-behind of the App.xaml:
public partial class App : Application
{
public App()
{
//...
this.UnhandledException += this.Application_UnhandledException;
InitializeComponent();
}
private void Application_UnhandledException(object sender, ApplicationUnhandledExceptionEventArgs e)
{
if (!System.Diagnostics.Debugger.IsAttached)
{
//...
e.Handled = true;
}
}
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
If you want to log the exception directly when it occurs, you can call the Logging Service in the catch block of your try and catch block.
Note: If you applications will throw a lot of exceptions, you will get a lot of service calls, which can affect performance, so in that case it can be advisable to save exceptions for example into a variable or Isolated Storage, so send them all in a “batch”.
Here is an example how you can use either the DomainService or the WCF Service in the Application_UnhandledException:
WCF Service:
private void Application_UnhandledException(object sender, ApplicationUnhandledExceptionEventArgs e)
{
if (!System.Diagnostics.Debugger.IsAttached)
{
var logServiceClient = new LogServiceClient();
logServiceClient.LogCompleted += (s, ae) =>
{
if (ae.Error != null)
{
//...
}
};
logServiceClient.LogAsync(e.ExceptionObject.ToString());
e.Handled = true;
}
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
WCF RIA Services:
private void Application_UnhandledException(object sender, ApplicationUnhandledExceptionEventArgs e)
{
if (!System.Diagnostics.Debugger.IsAttached)
{
var logDomainContext = new LogDomainContext();
logDomainContext.Log(e.ExceptionObject.ToString(),
invokeOperation =>
{
if (invokeOperation.HasError)
{
//....
}
},
null);
e.Handled = true;
}
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
As you can see the async. call to the Services is only made if no debugger is attached when running the application. There is no reason to log exception during debug mode.
Because a Service will do a call over the network, the Service can also fail, for example connection problem to the server, or the service it self thrown an exception while trying to log messages etc. If the Service fails, the LogCompleted event of the WCF Service or the InvokeOperation callback of a WCF RIA Services can be used to check if there was any exception while calling the services. As you have probably notice, there is no code added for handling the exception when calling a Service in my examples, it’s because you maybe want to handle exceptions in different ways, so I left it for you to implement it. What you can do is for example add a MessageBox showing a user friendly message that the logging fails, but that is kind of strange message to show the user. Another solution could be to store the original exception to the Isolated Storage, and try to resend the messages stored in the Isolated Storage later and hope it will be sent the next time without any problems. But I should added a solution where the user can decide if the exception and information should be sent for logging and that is what the rest of the blog post is about.
Using Child Window to show and send exceptions to the service
When an exception occurs in the Windows OS we will see a dialog with the exception and also detailed information, we can also decide if we want to send the log to Microsoft or not. This will let the users know what kind of information that will be sent and also let the user know that the application will send information. By sending information without letting the user knows about it can be kind of bad thing to do (If the user haven’t already agree on it). So instead of just sending the message to a Service we can show a ChildWindow when an exception occurs, for example a window like this:
Here is the XAML for the above ChildWindow:
<controls:ChildWindow
xmlns:controlsToolkit="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Toolkit" x:Class="SilverlightApplication50.ExceptionWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls"
Width="400" Height="300"
Title="Exception occured">
<Grid x:Name="LayoutRoot" Margin="2">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel>
<TextBlock FontSize="12" FontWeight="Bold" Text="An exception occured"/>
<TextBlock
x:Name="userFriendlyMessageTextBlock"
Margin="0,10,0,0"
FontStyle="italic"
TextWrapping="Wrap"
Text="User friendly message"/>
<controlsToolkit:Expander Height="160" Header="More information" Margin="0,10,0,0">
<controlsToolkit:Expander.Content>
<TextBox
x:Name="detailedInforTextBox"
AcceptsReturn="True"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto"
Text="A more detailed information about the exception"/>
</controlsToolkit:Expander.Content>
</controlsToolkit:Expander>
</StackPanel>
<Button x:Name="SaveLogButton" Click="SaveLogButton_Click" Content="Save Log".../>
<Button x:Name="CancelButton" Content="Don't Send" Click="CancelButton_Click" ... />
<Button x:Name="OKButton" Content="Send" Click="OKButton_Click" ... />
</Grid>
</controls:ChildWindow>
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Note: The Silverlight Controls Toolkit is used here to add the Expander control.
The ChildWindow will have an Error property added in the Code-Behind so we can pass the Exception to the ChildControl when it’s created and also a proeprty for an user friendly message and for a detailed message. When the user press the Ok button the message will be sent to a Service. The following is an example where the WCF RIA Services LogDomainService is used (To use the WCF Service, just replace the code in the OKButton_Click to call the WCF Service and add the LogCompleted event to check for an exception):
public partial class ExceptionWindow : ChildWindow
{
private Exception _exception;
public ExceptionWindow()
{
InitializeComponent();
}
public string UserFriendlyException
{
get { return userFriendlyMessageTextBlock.Text; }
set { userFriendlyMessageTextBlock.Text = value; }
}
public string DetailedException
{
get { return detailedInforTextBox.Text; }
set { detailedInforTextBox.Text = value; }
}
public Exception Error
{
set { _exception = value; }
}
private void OKButton_Click(object sender, RoutedEventArgs e)
{
var errorMsg = FormatMessage();
var logDomainContext = new LogDomainContext();
logDomainContext.Log(errorMsg,
invokeOperation =>
{
if (invokeOperation.HasError)
{
if (MessageBox.Show("Error while trying to send the log, do you want to save it temporary for a later try?", "Error sending log", MessageBoxButton.OKCancel) == MessageBoxResult.OK)
IsolatedStorageHelper.SaveToIsolatedStorage("retry_sending_exception.log", errorMsg);
invokeOperation.MarkErrorAsHandled();
}
},
null);
this.DialogResult = true;
}
private string FormatMessage()
{
string originalException = string.Empty;
if (_exception != null)
originalException = _exception.ToString();
return string.Format("{0} - {1}\n\nDetailed Message:\n{2}\n\nOriginal Exception:\n{3}",
DateTime.Now.ToString(),
UserFriendlyException,
DetailedException,
originalException);
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
this.DialogResult = false;
}
//...
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
The OKButton_Click event handler will call the Logging Service Log method, if the call fails a MessageBox will appear and the use can decide if they want to save the original exception temporary. The IsolatedStorateHelper is a simple class I have added which will save the errorMsg to the Isolated Storage:
public void SaveToIsolatedStorage(filenName, string errorMsg)
{
using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())
{
using (IsolatedStorageFileStream isfs = new IsolatedStorageFileStream(
fileName,
FileMode.Create,
isf))
{
using (StreamWriter sw = new StreamWriter(isfs))
{
sw.Write(errorMsg);
sw.Close();
}
}
}
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Note: The Isolated Storage only allow use to use the FileMode Create, so we can’t use the Append mode, so only the latest failed logging will be saved.
The Application_Startup event handler in the App.xaml.cs can be used to try to resend the latest exception, here is an example for doing that:
public partial class App : Application
{
public App()
{
this.Startup += this.Application_Startup;
//...
}
private void Application_Startup(object sender, StartupEventArgs e)
{
TryToResendLatestException();
this.RootVisual = new MainPage();
}
private void TryToResendLatestException()
{
var logServiceContext = new LogDomainContext();
var errorMsg = IsolatedStorageHelper.LoadStringFromIsolatedStorage("retry_sending_exception.log");
if (!string.IsNullOrEmpty(errorMsg))
{
logServiceContext.Log(errorMsg,
invokeOperation =>
{
if (!invokeOperation.HasError)
IsolatedStorageHelper.DeleteFileFromIsolatedStorage("retry_sending_exception.log");
invokeOperation.MarkErrorAsHandled();
},
null);
}
}
//...
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Here is the IsolatedStorageHelper’s Load and Delete methods:
public string LoadStringFromIsolatedStorage(string fileName)
{
using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())
{
if (isf.FileExists("retry_sending_exception.log"))
{
using (IsolatedStorageFileStream isfs = new IsolatedStorageFileStream(
fileName,
FileMode.Open,
isf))
{
using (StreamReader sr = new StreamReader(isfs))
{
return sr.ReadToEnd();
}
}
}
}
return null;
}
public void DeleteFileFromIsolatedStorage(string fileName)
{
using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())
{
if (isf.FileExists(fileName))
isf.DeleteFile(fileName);
}
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
The ChildWindow also has a “Save Log” button, here is the code to save the exception message to the client's local disc (I cut it out from the above ChildWindow example to not making the code too big, I could of course used some Refactoring here to make the file smaller):
private void SaveLogOnClientDisc(string errorMessage)
{
var saveLogDialog = new SaveFileDialog();
saveLogDialog.DefaultExt = ".log";
saveLogDialog.Filter = "Text Files|*.txt|Log Files|*.log|All Files|*.*";
saveLogDialog.FilterIndex = 2;
bool? dialogResult = saveLogDialog.ShowDialog();
if ( dialogResult == true )
{
try
{
var contents = Encoding.Unicode.GetBytes(errorMessage);
using (var fileStream = saveLogDialog.OpenFile())
{
fileStream.Write(contents, 0, contents.Length);
fileStream.Close();
MessageBox.Show("File successfully saved!");
}
}
catch ( Exception ex )
{
MessageBox.Show("Can't save file: " + ex.Message);
}
}
}
private void SaveLogButton_Click(object sender, RoutedEventArgs e)
{
SaveLogOnClientDisc(FormatMessage());
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
The following is the code added to the Application_UnhandledException to show the Exception dialog:
private void Application_UnhandledException(object sender, ApplicationUnhandledExceptionEventArgs e)
{
if (!System.Diagnostics.Debugger.IsAttached)
{
var exceptionDialog = new ExceptionWindow();
exceptionDialog.UserFriendlyException = e.ExceptionObject.Message;
exceptionDialog.DetailedException = e.ExceptionObject.ToString();
exceptionDialog.Error = e.ExceptionObject;
exceptionDialog.Show();
e.Handled = true;
}
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Note: I could have skipped adding the UserFriendlyException and DetailedException property and only use the Error property. But I decided to use them in this example so I can reuse the dialog for try and catch blocks, where I don’t want to show messages out from the exception thrown, for example:
try
{
//.. Do something
}
catch (Exception e)
{
var exceptionDialog = new ExceptionWindow();
exceptionDialog.UserFriendlyException = "Can't create user 'John Doe'";
exceptionDialog.DetailedException = "The call to the CreateUser method failed because...;
exceptionDialog.Error = e;
exceptionDialog.Show();
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Another kind of logging solutions
With Silverligth 4.0 we can use COM, so we could for example open up Outlook and let the user send the error log via e-mail. We can also in Silverlight 3 do something similar by adding a client-side script and call it from the Silverlight app to open up the user’s e-mail client. In Silverlight 4 in a OOB and trusted mode, we can store log files to the users local disc.
Summary
In this post I wanted to show you different ways of logging client-side exceptions by calling a Service, either by using WCF or WCF RIA Services. Something to have in mind is that the call to the log service can fail, so have a backup plan to save the original exception on the client-side, for example by using the Isolated Storage to temporary store the log and try to resend it later, for example next time the Silverlight application is started.
If you want to know when I publish new blog posts, you can follow me on twitter: http://www.twitter.com/fredrikn
|
-
Note: This is post is based on the WCF RIA Services Beta for VS 2008 and changes can be done before WCF RIA Services hits RTM. There are several post on the forums about changing the default number of items passed passed from a DomainService to the client, so I decided to write a short blog post about how to configure the WCF Service of the WCF RIA Services. If you want to know how the WCF RIA Services adds default endpoints etc, you can take a look at my Deep Dive post. Have the following in mind when creating your DomainService: There is a wire between the server and the client, so be careful and not pass too much data over the wire. In a enterprise application, an app with a lot of users or low bandwidth it’s advisable to create a light weight class (Data Transfer Object) instead of instead of just passing a DAL type generated from Linq to SQL, Entity Framework or a domain entity. Only pass the data the View needs, this will reduce the number on data passed over the wire. I have helped a lot of customers lately which has major performance issue because they exposes DAL types or domain entities over the wire. There is a reason why for example WCF uses a 65kb message limit by default. It’s to make sure we open up our eyes and notice the network which bounds our client with the server. If you really need to pass a lot of data over the wire, you may notice that you will get an exception when you have exceeded the default number of item which can be serialized. You can change the maxItemsInObjectGraph size by configuring the WFC RIA Services behavior in web.config, by default this value is 65536 items. By changing the size of how many items which can be serialized and deserialized, you can “register” your service and add a behavior configuration for your service: <system.serviceModel>
<!— ... -->
<services>
<service name="SilverlightApplication.Web.MyDomainService"
behaviorConfiguration="MyDomainService_BehaviorConfig">
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="MyDomainService_BehaviorConfig">
<dataContractSerializer maxItemsInObjectGraph="xxxxxx"/>
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
The Service element name is the “Namesapce.DomainServiceName”. You don’t need to change the MaxReceivedMessageSize of the binding, because by default it’s set to: 2 147 483 647 bytes.
|
-
Note: The examples in this blog post are based on the WCF RIA Services PDC beta, and changes to the framework can be done until it hits RTM. In a preview blog post I wrote about how to handle exception when using .NET RIA Services and the Load method. This blog post will be about the same but instead based on the WCF RIA Services. If you want add a generic way to log exceptions thrown on the server-side, you can override the DomainService OnError method: [EnableClientAccess()] public class DomainService1 : DomainService { public IEnumerable<Customer> GetCustomers() { throw new ApplicationException("My exception"); }
protected override void OnError(DomainServiceErrorInfo errorInfo) { //Log exception errorInfo.Error } }
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
When you make a call to the Load method of the DomainContext on the client-side, by default an exception will be thrown when the Load operation is completed if an exception occurs on the server-side. If you use Silverlight as the client and you don’t handle the exception on the client-side, the App’s Application_UnhandledException will be executed. Something to be aware of is that the WCR RIA Services will use the customErrors section in the web.config to pass a detail server-side exception to the client, or not.
<customErrors mode="On" defaultRedirect="GenericErrorPage.htm"/>
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
If customErrors is on or remoteOnly (and you aren’t running the app locally on the remote machine), the exception throw on the server-side will not be passed to the client. You will still get an exception, but the information you will get is the name of the server-side method that throw an exception “Load operation failed for query ‘GetCustomers’. …..”. The type of the exception is System.Windows.Ria.DomainOperationException. You will get the same exception type even if the customErrors mode is set to Off or remoteOnly (When you are running the app locally on the remote machine), but after the name of the method, you will also get the server-side exception message “Load operation failed for query ‘GetCustomers’: My exception”.
NOTE: Don’t include sensitive information in the exception message that can be used by a hacker, so think through what kind of message you want to send to the client. In most cases a simple message like “Retrieving customers failed, please try again, if you see the same message please contact an administrator”. Make sure you log the original message so you have something to analyze if a user will contact you.
There are several of ways to get and check if a server-side exception is thrown when using the DomainService Load method. You can either use the LoadOperation object returned from the Load method, and hook up to its completed event, or pass in a callback to the Load method. I prefer to use a callback. Here is an example where an MessageBox will show an exception message if the GetCustomers method fails:
customerDomainContext.Load<Customer>(ds.GetCustomersQuery(), loadOperation => { if (loadOperation.HasError) { MessageBox.Show(loadOperation.Error.Message); loadOperation.MarkErrorAsHandled(); } } ,null);
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
The LoadOperation has a HasError property, you can use this one to see if the load operation fails based on an exception throw form the server-side. You can then use the Error property of the LoadOperation to get the error message from the server-side (Remember the customErrors mentioned earlier, it can prevent you from getting the message throw from the server-side). By using the LoadOperation’s MarkErrorAsHandled method, you will tell the WCR RIA Services that you have handled the exception, no reason for passing it along. There is also property which you can use to see or specify that the exception is handled, and the property is IsErrorHandled.
If you want to know when I publish a new blog post, you can follow me on twitter: http://www.twitter.com/fredrikn
|
-
Note: The examples in this blog post uses WCF RIA Services PDC Beta, and changes can be made in the future to the framework.
In several of my blog posts about WCF RIA Services I have used the Repository pattern to return an IEnumerable<T>, for example:
public class CustomerRepository : ICustomerRepository
{
public IEnumerable<Customer> GetCustomers()
{
//...
}
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
And in the DomainService:
[EnableClientAccess()]
public class CustomerService : DomainService
{
private ICustomerRepository _customerRepository;
public CustomerService(ICustomerRepository customerRepository)
{
_customerRepository = customerRepository;
}
public IEnumerable<Customer> GetCustomers()
{
//Use _customerRepository
}
}
Here is the client-side code which Loads the DTO/”Presentation Model” into the DomainContext:
var customerContext = new CustomerContext();
customerContext.Load<Customer>(customerContext.GetCustomers());
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
The Load method takes a Query object as an argument, by doing so, it knows which method in the DomainService it should call to load the objects. We can on the client-side specify our own query and pass it down to the server, and WCF RIA Services will make sure the execution of the Query will take place on the server side, this will reduce the data passed over the wire to the client. BUT! There is a problem of using IEnumerable<T>. The query will take place on top of the IEnumerable<T>, which means that our Repository will return ALL customers from the database, and this can affect performance (I talked to a well known SQL Server MVP about retrieving too much data from the database, he told me that he never notice a problem of retrieving to much data from a database, there are other issues that can affect performance badly.). So in that case, we don’t need to be worry?! I still think we should try to avoid retrieving data we don’t need from the database.
How can we solve the above issue with IEnumerable<T>?
We can for example get the query passed down to our DomainService and pass it to our Repositories, in that case we can use it as a Query Object. We can get the Query information by overriding the Query method of the DomainService class. Another solution is to create our own Query Object and pass it to the server as an argument to the GetCustomers method or just simply use the IQueryable<T> interface.
How can IQueryable<T> be used?
When using Entity Framework or Linq To SQL etc, we can create a query, this query will only be executed when we tell it to, for example calling the ToList method, Single etc of the query object:
var customersQuery = from customer in myContext.Customers
where customter.Country == "Sweden"
orderby customer.CompanyName select customer;
var results = customersQuery.ToList(); //Execute the query here
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
As long as we don’t execute the customerQuery, we can extend the query, when we are done we execute it. When WCF RIA Services uses the EF or L2S Domain Service classes, it will only return the query (in this case use the IQueryable<T>) and not a list of objects, so when we use the Load method and pass down a query, the query will extend the returned query, and then WCF RIA Services will invoke the query and EF and L2S will turn the query into T-SQL and execute it against the database. In that way we can reduce the data returned from our database to the DomainSerivce.
This sounds great, let us always return IQueryable<T>!
But! and a big one, we can’t use this solution in a RIA where business logic should be on the server-side. In some cases we may need to perform some business logic based on the data retrieved from the database, in this case we need to execute the query before returning our data to the client. So in this case we need to use IEnumerable<T>. But (a lot of but here), if we don’t need to perform any business logic on the server-side before we return our objects to the client, we can use the IQueryable<T>, I think it’s a nice solution for small client/server apps. It requires that the objects we want to send to the client are well designed and with the View in consideration. If we use the IQueryable<T>, we can’t use the Repository pattern, instead we can use a Query provider. So the Repository turns into a query provider:
public class CustomerQueryProvider : ICustomerQueryProvider
{
public IQueryable<Customer> GetCustomers()
{
//...
}
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
The CustomerQueryProvider can for example use EF or L2S to return an IQueryable<T> of customers:
public IQueryable<Customer> GetCustomers()
{
_myObjectContext.Customers;
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
The DomainService will look like this:
[EnableClientAccess()]
public class CustomerService : DomainService
{
private ICustomerQueryProvider _customerQueryProvider;
public CustomerService(ICustomerQueryProvider customerQueryProvider)
{
_customerQueryPrivider = customerQueryProvider;
}
public IQueryable<Customer> GetCustomers()
{
//Use _customerQueryProvider.GetCustomers()
}
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
The reason why a Query Provider is used here, is to remove dependencies to a specific detail. Now we can have different Query Providers as long as they implements the ICustomerQueryProvider interface and the infrastructure used in the Query Provider supports the IQueryable<T> interface.
Summary
For small database driven RIA where almost not business logic is needed, we can get a lot of benefits of using the IQueryable<T> interface instead of the IEnumerable<T>. But in a enterprise RIA where business logic should take place on the server-side, the IEnumerable<T> can be a better choice. But it depends! There are so many factors that must be known before we can know which design will fit best.
If you want to know when I publish new blog posts, you can follow me on twitter: http://www.twitter.com/fredrikn
|
-
Note: The example in this blog post uses the WCF RIA Services PDC Beta, so things can be changed to the final version. In a preview post I wrote about how to create a DomainServiceFactory where Microsoft Patterns & Practices Unity Dependency Injection framework is used to create a DomainService, to make sure it will inject all it dependencies. This post will be about using StructureMap instead. Before you read this post, I would like you to read my preview post to understand how to create a DomainServiceFactory, because this post will only show you the code to make sure StructureMap can be used instead of Unity. The first thing I did was to add a ContainerBootstrapper, where I register all my types. In this case a ICustomerRepository and CustomerService:
public static class ContainerBootstrapper
{
public static void BootstrapStructureMap()
{
ObjectFactory.Initialize(x =>
{
x.ForRequestedType<ICustomerRepository>().TheDefaultIsConcreteType<CustomerRepository>();
});
}
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Note: I don’t need to add the Concrete type of CustomerService, StructureMap can by default create an instance and if there are any dependencies register, it will inject them, that is why only the ICustomerRepository is registered.
Here is the skeleton of the CustomerService:
[EnableClientAccess()]
public class CustomerService : DomainService
{
public CustomerService(ICustomerRepository customerRepository)
public CustomerPM GetCustomerByID(int customerID)
public void UpdateCustomer(CustomerPM customer)
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
And here is the ICustomerRepository interface:
public interface ICustomerRepository
{
Customer GetCustomerByID(int customerID);
void Update(Customer customer);
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
The ContainerBootstrapper’s BootstrapStructureMap method should be called when the application is startup or initialized, I decided to use the Application_Start in Global.asax in this example.
protected void Application_Start(object sender, EventArgs e)
{
DomainService.Factory = new StructureMapDomainServiceFactory();
ContainerBootstrapper.BootstrapStructureMap();
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
The DomainService.Factory is set to a new instance of a StructureMapDomainServiceFactory, the StructureMapDomainServiceFactory will be responsible to use StructureMap’s factory to create a DomainService. Here is the implementation of the StructureMapDomaiNServiceFactory:
public class StructureMapDomainServiceFactory : IDomainServiceFactory
{
public DomainService CreateDomainService(Type domainServiceType, DomainServiceContext context)
{
var service = ObjectFactory.GetInstance(domainServiceType) as DomainService;
service.Initialize(context);
return service;
}
public void ReleaseDomainService(DomainService domainService)
{
domainService.Dispose();
}
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
By using the ObjectFactory’s GetInstance, we can make sure StrucuterMap creates an instance of the specified type and then inject all dependencies.
Note: There is not exception handling added to the Factory, the reason is that the argument domainServuceType is already a DomainService type, if not the WCF RIA Services wouldn’t work. The GetInstance will throw an exception if the domainServiceType couldn’t be created, but I just let the exception bubble up. It’s up to you to add the exception handling you want to use.
If you want to know when I publish new blog posts, you can follow me on twitter: http://www.twitter.com/fredrikn
|
-
Note: The code example in this post uses Silverlight 4 PDC Beta and WCR RIA Services PDC Beta, so some changes can be made before RTM. In my previous post I wrote about “WCF RIA Services and a guide to use DTO/”Presentation Model””, and mention that I will later write a blog post about using a ViewModel. In this post I will show you how you can create a ViewModel and use Silverlight 4 Commanding to use the MVVM pattern. In this blog post I will use Unity as my Dependency Injection framework to inject my Repository to the DomainService, to make this possible we need to create our own DomainServiceFactory, check out my “WCF RIA Services Unity DomainServiceFactory” for information about how to use Unity and Dependency Injection, because I will not cover it in this post. Architecture and design in this post The following figure will show you the architecture and design I often use when building Rich Internet Applications:  In this post the Service Layer will be a DomainService using WCF RIA Services, the Domain Model will be a very simple, just have a Customer entity and a CustomerRepository. The follow if a class diagram of the Customer entity: The following code is the CustomerRepository interface: public interface ICustomerRepository
{
Customer GetCustomerByID(int customerID);
void Update(Customer customer);
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Note: This post will not focus on how the ICustomerRepository is implemented, assume it used Entity Framework, Linq to SQL, nHibernate or ADO.NET etc. It’s not important to know in this blog post.
The following code is the Service Layer, where WCF RIA Services DomainService is used and where the ICustomerRepository should be injected:
[EnableClientAccess()]
public class CustomerService : DomainService
{
private ICustomerRepository _customerRepository;
public CustomerService(ICustomerRepository customerRepository)
{
_customerRepository = customerRepository;
}
public CustomerDto GetCustomerByID(int customerID)
{
var customer = _customerRepository.GetCustomerByID(customerID);
return MapCustomerToCustomerDto(customer);
}
public void UpdateCustomer(CustomerDto customer)
{
if (customerDto == null)
throw new ArgumentNullException("customer");
_customerRepository.Update(MapCustomerDtoToCustomer(customerDto));
}
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
As you may notice there is a mapping (translator) between the domain Entity Customer to CustomerDto (Data transfer object). If we have used WCF, we would map our domain entity to the DataContract. Frameworks like the AutoMapper could for example be used here instead of manual mapping. In a small Intranet application where the internal bandwidth is good and we don’t have a lot of business logic, we could simply use our Customer and by pass the mapping. But it all depends on the application we are building, there are so many factors included before we can decide the proper design for our application. So this post is not showing any Silver Bullets, they aren’t any. The following is the code for the CustomerDto, some validation annotation is used on the DTO, when using a ViewModel this validation could instead be added as “real” code to the ViewModel, if we do so, the validation will be executed when we write a test against our ViewModel. But in this example code, I decide that I don’t need to include data rules as part of the test.
public class CustomerDto
{
[Key]
public int CustomerID { get; set; }
[Required]
[StringLength(32, MinimumLength=2)]
public string FirstName { get; set; }
[Required]
[StringLength(32, MinimumLength = 2)]
public string LastName { get; set; }
public int Age { get; set; }
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Note: I will use my own DomainServiceFactory to make sure ICustomerRepository will be injected when the CustomerService is created, you can read more about it here.
Now the serer-side and Service layer is completed we can leave it and continue with the client implementation where a ViewModel is going to be used and also Silverlight 4’s Commanding.
ViewModel
A ViewModel is a representation of the View but as a class. Think of the movie Matrix, they only saw a lot of characters moving on the screens, but by looking at them, they saw the world. Think of the ViewModel class as the characters and by looking at it we should see the View. The reason to use a ViewModel is separation of concerns, we don’t want to have logic added to the view. With this separation we could also write automated test to test our View even if the UI isn’t in place.
Here is a skeleton of the ViewModel where Commanding is used:
public class CustomerViewModel : ViewModel
{
public CustomerViewModel()
{
}
public string FirstName
{
get;
set;
}
public string LastName
{
get;
set;
}
public bool IsAdult
{
get;
}
public bool IsLoading {
get; internal set; }
public ICommand Save
{
get;
}
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
The ViewModel represents a View which should display two Input fields for FirstName and LastName, and also a read-only checkbox if the Customer is an adult or not. The View should also have a Save button. The IsLoading is used to show or hide a BusyIndicator. Here is the UI of the View which will use the ViewModel:
The following is the XAML for the View, where no code-behind is needed, instead a ViewModel is added as a resource and the data binding feature is used to bind the View to the ViewModel:
<UserControl
xmlns:my="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Toolkit" x:Class="SilverlightApplication1.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"
xmlns:vm="clr-namespace:SilverlightApplication1.ViewModels"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">
<UserControl.Resources>
<vm:CustomerViewModel x:Name="customerViewModel"/>
</UserControl.Resources>
<Grid x:Name="LayoutRoot" DataContext="{StaticResource customerViewModel}">
<my:BusyIndicator IsBusy="{Binding IsLoading}"></my:BusyIndicator>
<TextBox Text="{Binding FirstName, Mode=TwoWay, NotifyOnValidationError=True}" ... />
<TextBox Text="{Binding LastNaem, Mode=TwoWay, NotifyOnValidationError=True}" ... />
<TextBlock Text="First Name:" .../>
<TextBlock Text="Last Name:" .../>
<CheckBox IsChecked="{Binding IsAdult}" Content="Is Adult" .../>
<Button Command="{Binding Save}" Content="Save" ... />
</Grid>
</UserControl>
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
ViewModel implementation
You have probably notice that the CustomerViewModel inherits the ViewModel class, the ViewModel class implements the the INotifyPropertyChanged and INotifyDataErrorInfo. You can find the implementation of the ViewModel on my other post about INotifyDataErrorInfo class. In the CustomerViewModel constructor there is a call to the WCF RIA Services to get a specific Customer, because the operation is asynchronous we can’t know how long it will take before the Customer is loaded, so the IsLoading property is set to true to show the BusyIndicator. Here is the whole implementation of the CustomerViewModel:
public class CustomerViewModel : ViewModel, ISaveableViewModel
{
CustomerContext _customerContext = new CustomerContext();
CustomerDto _customerDto = new CustomerDto();
bool _isLoading = false;
public CustomerViewModel()
{
this.IsLoading = true;
_customerContext.Load<CustomerDto>( _customerContext.GetCustomerByIDQuery(10),
loadOperation =>
{
_customerDto = loadOperation.Entities.SingleOrDefault();
LoadingCompleted();
}, null);
}
public string FirstName
{
get { return _customerDto.FirstName; }
set
{
if (_customerDto.FirstName != value)
{
_customerDto.FirstName = value;
NotifyPropertyChanged("FirstName");
}
}
}
public string LastName
{
get { return _customerDto.LastName; }
set
{
if (_customerDto.LastName != value)
{
_customerDto.LastName = value;
NotifyPropertyChanged("LastName");
}
}
}
public bool IsLoading
{
get { return _isLoading; }
internal set { _isLoading = value; NotifyPropertyChanged("IsLoading");
}
}
public bool IsAdult
{
get { return _customerDto.Age >= 18; }
}
public ICommand Save
{
get { return new SaveCommand(this); }
}
internal void SaveCustomer()
{
_customerContext.SubmitChanges();
}
private void LoadingCompleted() { NotifyPropertyChanged("FirstName"); NotifyPropertyChanged("LastName"); NotifyPropertyChanged("IsAdult"); this.IsLoading = false; }
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Note: The above CustomerViewModel is not designed for testability, if we want to write a unit test to test the ViewModel, we could for example pass a DomainClient to the CustomerContext constructor when it’s created, it can be done in several ways, one way is for example to pass the DomainClient as an argument to the CustomerViewModel constructor, but then we have to use code-behind to create the CustomerViewModel or adding our own AttachedProperty. We can also add a DomainClient as a resources in XAML and use property injection to inject the DominContext to the ViewModel.
Something that may seems strange is the empty CustomerDto assigned to the _customerDto field. The other properties in the CustomerViewModel will access the _customerDto and its properties. To avoid adding a lot of if statements to check for null in both the get and set, a default CustomerDto is used. So it’s only a “ugly” hack to avoid a lot of code in each property. The load operation of a Customer will take place in the default constructor of the CustomerViewModel:
public CustomerViewModel()
{
this.IsLoading = true;
_customerContext.Load<CustomerDto>(
_customerContext.GetCustomerByIDQuery(10),
loadOperation =>
{
_customerDto = loadOperation.Entities.SingleOrDefault();
LoadingCompleted();
}, null);
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
The IsLoading property is set to true to make sure the BusyIndicator will be displayed when the loading takes place. When the loading of the Customer is completed the _customerDto will be set to the loaded customer and the LoadingCompleted method will be called. This method will make sure the BusyIndicator will be hidden and also notify the View about changes, so the bindings to the controls will call the get methods of bounded properties to get the loaded customer’s information and show it. The last part to cover is the use of Commanding in Silverlight 4.
Commanding
You can read about Commanding in my post “Silverlight 4 Commanding enables ViewModels”. In the ViewModel there are one method SaveRule, this is from the interface ISaveableViewModel implemented by the CustomerViewModel. There is also a Save property added to the CustomerViewModel, this property will return a SaveCommand where the ViewModel is passed as an argument.
public ICommand Save
{
get { return new SaveCommand(this); }
}
internal void SaveRule()
{
_customerContext.SubmitChanges();
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
The Save property is bounded to the View’s Save button’s command property. Here is the implementation of the SaveCommand returned from the Save property:
public class SaveCommand : ICommand
{
private ISaveableViewModel _view;
public SaveCommand(ISaveableViewModel view)
{
_view = view;
}
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
_view.SaveRule();
}
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
As you can see the SaveCommand takes a ISaveableViewModel interface (Yes I know the name of the interface may be strange, but I’m not so good at naming ;)). The idea is to make sure the operation the Command should execute is added to the ViewModel itself, by doing so the Command can be reused by several other Views.
Summary
This post was about giving you the ideas of how you can use WCF RIA Services, the Model View View Model pattern and Commanding. The code used in this example is not perfect, no specific error handling was added more than using the WCF RIA Services validation annotation.
If you want to know when I publish a new blog post, then follow me on twitter: http://www.twitter.com/fredrikn
|
-
Note: This post is based on WCR RIA Services PDC beta and changes can be made in a future release.
In this post I will show you how it’s possible to create your own DomainServiceFactory which has the responsibility to create a DomainService instance. I decided to use Unity as a Dependency Injection framework to create my DomainServices. Why do we even want to create a custom DomainServiceFactory, well, to avoid dependencies inside our DomainServices, for example here is a DomainService which has a dependency to a CustomerRepository: [EnableClientAccess()]
public class CustomerService : DomainService
{
public CustomerPM GetCustomerByID(int customerID)
{
var customerRepository = new CustomerRepository();
var customer = customerRepository.GetCustomerByID(customerID);
return new CustomerPM()
{
CustomerID = customer.CustomerID,
FullName = customer.FirstName + " " + customer.LastName,
Age = customer.Age
};
}
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
The code above will break the Dependency Inversion Principle, High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend upon details. Details should depend upon abstractions. We can’t use the code above in a unit-test, because it will use the CustomerRepository, so in that case we are doing an integration test. Our DomainService is now tightly coupled with the CustomerRepository. So to remove the detail (CustomerRepository) and use an abstraction, we can use the following solution: [EnableClientAccess()]
public class CustomerService : DomainService
{
private ICustomerRepository _customerRepository;
public CustomerService(ICustomerRepository customerRepository)
{
_customerRepository = customerRepository;
}
public CustomerPM GetCustomerByID(int customerID)
{
var customer = _customerRepository.GetCustomerByID(customerID);
return new CustomerPM()
{
CustomerID = customer.CustomerID,
FullName = customer.FirstName + " " + customer.LastName,
Age = customer.Age
};
}
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Now the detail is removed, and we work against an abstraction (ICustomerRepository). We can now easily use this DomainServices with different kind of CustomerRepository as long as they implements the ICustomerRepository. We can now create our a test stub for the CustomerRepository when we write our unit-test etc. The problem with the code above, is that WCF RIA Services will throw an exception when it will try to create an instance of the DomainService. It will by default use the default constructor. If we add a default constructor, there is no way for the WCF RIA Services to know that we need to pass in a ICustomerRepository, so our GetCustomerByID method will throw and null reference exception when we try to call the CustomerRepository’s GetCustomerByID method. To solve this we can create our own DomainServiceFactory, where we handle the creation of the DomainService.
To create a DomainServiceFactory we need to create a class that implements the IDomainServiceFactory interface, and implement two methods, CreateDomainService and ReleaseDomainService:
public class MyDomainServiceFactory : IDomainServiceFactory
{
public DomainService CreateDomainService(Type domainServiceType, DomainServiceContext context)
{
var service = Global.UnityContainer.Resolve(domainServiceType) as DomainService;
service.Initialize(context);
return service;
}
public void ReleaseDomainService(DomainService domainService)
{
domainService.Dispose();
}
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
The CreateDomainSerivce method will be called when WCF RIA Services will create an instance of our DomainSerivce, and the ReleaseDomainService will be called when WCF RIA Services will release it. To make the factory work, we must call the Initialize method of the DomainService we have created before returning it. The code above will use Unity to create an instance of a DomainService and if they have any dependencies, Unity will inject them if they are registered to the UnityContainter.
Note: Once again I think Microsoft makes a mistakes with factories, the ASP.NET MVC ControllerFactory first took a type of the Controller as an argument, which was later changed to a string. Here in the WCR RIA Services, they passing a type again. I hope this will be solved in the future release so they pass a string instead. I think it’s the DomainServiceFactory’s responsibility to locate the assembly where the DomainService is, and by a name create an instance. Why? Because some Dependency Injection framework wants to use a string to locate a type within a container, and some framework can also handle the creation of an instance.
To register our IDomainServiceFactory we use the DomainSerivce.Factory property, which is a static property, we can for example use it in Application_Start in global.asax to configure the use of our factory:
public static UnityContainer UnityContainer = new UnityContainer();
protected void Application_Start(object sender, EventArgs e)
{
DomainService.Factory = new MyDomainServiceFactory();
UnityContainer.RegisterType<CustomerService>();
UnityContainer.RegisterType<ICustomerRepository, CustomerRepository>();
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Note: For demonstartion purpose I simply add a static UnityContainer to the Global.asax file, and in the MyDomainServiceFactory I access the UtnityContainer by writing Global.UnityContainer.
I also register my DomainService and Repositories in Global.asax.
Now when WCF RIA Services wants to create a DomainService, it will use the MyDomainServiceFactory, which will use Unity to create an instance of the DomainService, and CustomerRepository will be injected to the CustomerService.
Something I want to see in the future, is that Microsoft makes sure we can create our own DomainService method Invoker, or at least add two public events, OnOperationExecuting and OnOperationExecuted, so we can simply add pre- and post conditions, or have more possibility to extend the WCF RIA Services.
If you want to know when I publish a new blog post, you can follow me on twitter: http://www.twitter.com/fredrikn
|
-
NOTE: Examples in this blog post is based on the VS 2010 Beta 2, WCF RIA Services PDC Beta, some changes to the WCF RIA Services can happen until RTM. I have seen several questions and request about how to use Data Transfer Objects (DTO), or a ”Presentation Model” (Model created for presentation purpose) with WCF RIA Services. In this post I will write something about it. Why use DTO/”Presentation Model”?
Why use DTO/”Presentation Model” at all? Well, domain entities aren’t design with presentation in mind; they are design to solve business problems. Because of that a domain entity is not created with presentation in mind and shouldn’t be passed to the View. If we do it, we will end up with a lot of logic in our view only to make sure our view will show the domain entities in the way we wants, and it can also ends up that we send more data than a view is needed. So with a DTO/”Presentation Model” we can create a new “model” with is designed with presentation in mind, and also will only contains the data the View needs.
My Windows to my logic
If we have a domain model in place or a simple n-tier architecture with a business logic layer and data access layer, we need to add a Service layer on top of them, and this layer is designed and created for our client in a Rich Internet Application only. The Service Layer will be our window to our logic. It’s the Service layer responsibility to communicate to our domain model or business logic, it also has the responsibility to only pass the data a client needs. This kind of data passed to the client is a DTO/”Presentation Model”. The Service layer knows about the domain model and also the DTO/”Presentation Model”. The domain model doesn’t know about the Service layer. The client/view or should I say “presentation layer” will only know about the existents of the Service layers and its services it will use, and also the DTO/”Presentation Model”, it will not know about the domain model.
The above figure shows how I often design my RIA. But and a big BUT, I select the design which is best suited for the application I’m building. If you take a look at the figure above, you can see that the “DTO/PM” is also known to the View, this is not always the case, which is why there is a box which will stop at the ViewModel. I have seen several articles and blog post where they say they use a ViewModel, but what they do is to pass the DTO directly to the View for rendering. Is that wrong? I will not say it is, instead say “It depends”. If our DTO/”Presentation Model” is well designed for a specific View, it is ok to pass it along through a ViewModel, but if it isn’t, it’s not ok. The ViewModel in this case is similar to Martin Fowlers Presentation Model pattern. Some developers still uses code-behind and no ViewModel, so in that case the View will use the DTO.
Note: In this post I will not use the ViewModel, I will just simply use code-behind to call the Service layer, this post is only about how we can use DTO/”Presentation Model” with WCF RIA Services. If you want to read more about RIA Architecture etc, you can check our the following blog post: http://weblogs.asp.net/fredriknormen/archive/2009/04/19/ria-architecture-with-silverlight-in-mind.aspx
Creating a Service Layer with WCF RIA Services using DTO/”Presentation Model”
Before creating the Service Layer, we will take a look at the Entity I have in my domain model. The following is the View which should display the Customer: As you can see the View and the Customer entity are very different. If we pass our Customer to the View, we need to add logic which will combine the FirstName and LastName property, and also add logic which will check the checkbox Is Adult, if Age >= 18 (In Sweden you are an adult it you are 18 years old ). The View is not interested in the Address, Phone property, so why pass it to the View? So what we can do is to create a DTO/”Presentation Model” for the View. We can add this model in a separate class library or to the Web projects which will host our Silverlight application. In a small application I often only create a folder in my Web project called “PresentationModel” and put my DTO/”Presentation Model” classes to that folder. The reason I do that is because the DTO/”Presentation Model” I will create is only for my Silverlight application, not for other clients. Here is my DTO/”Presentation Model”: public partial class CustomerPM
{
[key]
public int CustomerID { get; set; }
public string FullName { get; set; }
public int Age { get; set; }
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Note: I will make the DTO/”Presentation Model” partial, the reason I do that is to make it possible to use the WCR RIA Services .shared feature.
The DTO/”Presentation Model” should only pass the data the view is needed, nothing more. The CustomerID is marked with the KeyAttribute, and we must at least mark one property to be a identifier of the object. WCF RIA Service’s DomainContext has an Identity Map, and it will cache objects based on the specified key. The View will check a CheckBox if the Customer is an adult. We can pass the Age along to the View and use a ViewModel to add the logic to know if the Customer is and adult or not, but in this case we will not use a ViewModel (I do prefer a ViewModel), we instead turn the DTO into a “Presentation Model” instead. So we will use the .shared feature to add an IsAdult property. A DTO will not have any kind of logic, it will only be used to pass data over the wire, so if we add logic to a DTO, it will instead be something else, and in this case I will say it will be a “Presentation Model”, a model used to simply the presentation of an domain entity. Here is the .shared code for the CustomerPM:
CustomerPM.shared.cs:
public partial class CustomerPM
{
public bool IsAdult
{
get { return this.Age >= 18; }
}
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Now when the “Presentation Model” is created we need to create our Service Layer. The Service Layer in this case is the WCF RIA Services DomainSerivce, so here is the DomainService which will use the “Presentation Model”:
[EnableClientAccess()]
public class CustomerService : DomainService
{
private ICustomerRepository _customerRepository;
public CustomerService()
{
_customerRepository = new CustomerRepository();
}
public CustomerService(ICustomerRepository customerRepository)
{
_customerRepository = customerRepository;
}
public CustomerPM GetCustomer(int customerID)
{
var customer = _customerRepository.GetCustomerByID(customerID)
return new CustomerPM()
{
CustomerID = customer.CustomerID,
FullName = customer.FirstName + " " + customer.LastName,
Age = customer.Age
};
}
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
As you can see a WCF RIA Services query method can return a simple object, in this case the CustomerPM. The Query method will make a call to our domain model to get the Customer entity.
Note: In the current WCR RIA Services PDC beta for VS 2008, you can’t return a single entity, but it can be done in the VS 2010 version.
The CustomerRepository’s GetCustomerByID method can use Entity Framework, Linq To SQL, nHibernate or normal ADO.NET etc to fill a Customer entities with data from a data source.
By using Object Initializer we can simply map our domain entity to our “Presentation Model”. Normally I will do some refactoring here, and create a MapCustomerToCustomerPM method, which will handle the mapping. As you can see the CustomerService will take a ICustomerRepository as an argument to the constructor, this constructor can be used when we write unit test to test the DomainService. The default constructor will create a “real” instance of the CustomerRepository. Normally I would use a Dependency Injection framework, like Unity or StructueMap etc, can also use MEF (which is not a “pure” DI framework) to inject a ICustomerRepostiroy when the DomainService is created, to make that possible when using WCF RIA Services, we can for example create our own IDomainServiceFactory, I prefer my own IDomainServiceFactory instead, I will post a blog post about that later.
Now when our DomainService is in place we can now use it from our Silverlight application.
Calling our DomainService from Silverlight
WCF RIA Services will generate classes for us on the client-side, it will create a state-full context class for us (DomainContext), which will have Dirty tracking, Unit or Work and a Identity Map, we will use that class to load our entities into the context. The name of the DomainContext will be CustomerContext. WCR RIA Services will take the name of the DomainService when it creates the DomainContext, if the name has the suffix “Service”, it will rename it to “Context” on the client-side, if not it will keep the name of the DomainService. The following code will make an asynchronous call to our DomainService and get a specific customer and set the LayoutRoot’s DataContext to the CustomerPM and use data-bounding:
var customerContext = new CustomerContext();
customerContext.Load<CustomerPM>(customerContext.GetCustomerByIDQuery(10),
loadOperation =>
{
LayoutRoot.DataContext = loadOperation.Entities.First();
}, null);
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Here is the XAML: <UserControl x:Class="SilverlightApplication1.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">
<Grid x:Name="LayoutRoot">
<StackPanel>
<TextBox Text="{Binding FullName}"></TextBox>
<CheckBox IsChecked="{Binding IsAdult}"></CheckBox>
</StackPanel>
</Grid>
</UserControl>
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Summary
As you may notice in this blog post, it’s quite easy to use DTP/”Presentation Model”, if we want to add validations to our DTO/”Presentation Model”, we can simply do that with annotation.
If you want to read more about DTO/”Presentation Model” you can take a look at the following posts:
http://weblogs.asp.net/fredriknormen/archive/2009/11/20/wcf-ria-services-and-dto-with-association.aspx
http://weblogs.asp.net/fredriknormen/archive/2009/11/20/wcf-ria-services-what-you-need-to-know-when-creating-dto-presentation-model.aspx
If you want to know when I have published a new blog post, you can follow me on twitter: http://www.twitter.com/fredrikn
|
-
Note: This is based on WCF RIA Services beta, and some stuffs can change in a future release. As many of you may know, WCF RIA Services uses WCF. WCF uses EndPoints where we specify the ABC (Address, Binding and Contract). By default WCF RIA Services uses its own ServiceHost, call DomainServiceHost. The DomainServiceHost will add EndPoints programmatically and it will add three kind of EndPoints, WebHttpBinding (for REST), BasicHttpBinding and BinaryHttpBinding. All binding will have a MaxReceivedMessageSize set to ”2147483647”. The address of the three default bindings added are the following: For WebHttpBinding: “baseAddress" (REST with JSON Endpoint) For BasicHttpBinding: “baseAddress” + “/soap” (SOAP with XML Endpoint) For BinaryHttpBinding: “baseAddress” + “/binary” (SOAP with Binary Endpoint) If you want to change the binding used, you can pass the address with any of the above prefix after the baseAddress to the constructor when you create the DomainContext on the client-side:
var myDomainContext = MyDomainContext( new Uri("SilverlightApplication1-Web-MyDomainService.svc/binary", UriKind.Relative)));
The security mode used on the bindings are “Transport” for https and the rest if a credential exists is “Transport credential only”. What's worry me is that the Metadata behavior’s HTTP get is enabled, so everyone can get information about the DomainService. Because of security reasons when using WCF, it’s advisable to turn it off in production.
WCR RIA Services uses a DomainServiceHostFactory which inherits from the SerivceHostFactory. The DomainServiceHostFactory has the following method:
protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses) { return new DomainServiceHost(serviceType, baseAddresses); }
The serviceType is the type of the service and the baseAddresses are an array of addresses which will be added as addresses to EndPoints. We can create our own ServiceHost if we want, or just create our own SerivceHost factory to just add more EndPoints for specific addresses, or filter out some addresses we don’t want to allow. If we creates our own ServiceHost, we need to inherits the ServiceHost base class, and also create our own ServiceHostFactory. Here is an example of a ServiceHostFacotry which will return a custom ServiceHost:
public class MyDomainServiceHostFactory : ServiceHostFactory { public MyDomainServiceHostFactory() {}
protected override ServiceHost CreateServiceHost( Type serviceType, Uri[] baseAddresses) { return new MyDomainServiceHost(serviceType, baseAddresses); } }
The MyDomainServiceHost could for example look like this:
public class MyDomainServiceHost : DomainServiceHost { public MyDomainServiceHost( Type domainServiceType, params Uri[] baseAddresses) : base( domainServiceType; baseAddresses) { }
protected override void AddEndpoints() { //...
base.AddServiceEndPoint(...); } }
Note: The DomainServiceHost, is the WCF RIA’s own custom ServiceHost.
We can then use our custom ServiceHostFactory by adding a .svc file to for our DomainServices to the Services directory (If you don’t have a Services directory in the root folder of your web project, just add one) and specify a Factory by using the Factory attribute of the ServiceHost directive:
<%@ ServiceHost Service="SilverlightApplication1.Web.MyDomainService" Factory="SilverlightApplication1.Web.MyDomainServiceHostFactory" %>
To get the name of the .svc file, you simply use the type of the DomainSerivce and replace “.” with a “-”. So if the type is as above “SilverlightApplication1.Web.MyDomainService”, the .svc file should have the following name: “SilverlightApplication1-Web-MyDomainService.svc”.
If you want to know when I post new blog posts, you can follow me on twitter: http://www.twitter.com/fredriknormen
|
-
Note: This post is based on the WCF RIA Services PDC Beta. There is a discussion about WCR RIA Services and how to update an EntitySet after a Submit changes, on the Silverlight forum. If we loads a set of entities they will be cached in an EntitySet on the client-side, so after a Submitting changes only the entities we have removed on the client will be removed from the client-side’s EntitySet. If someone else have removed some of the entities from the database we work against, we still have those cached on the client-side. At the moment there aren’t any reload features added to the WCF RIA Services, so we need to handle it by our self. We can’t just make a new call to the Load method of our DomainContext, it will only “merge” the existing EntitySet with changes or add new entities, but not removing anything. So what we need to do, is to implement code which will handle the removing of entites from the EntitySet. Here is an example code which will make sure removed entities (entities which is cached since an early Load operation but not part of the new Load operation) will be removed from the cached EntitySet: _domainContext.SubmitChanges(
submitOperation =>
{
_domainContext.Load<Customer>(
_domainContext.GetCustomersQuery(),
LoadBehavior.RefreshCurrent,
loadOperation =>
{
var results = _domainContext.Customers.Where(
entity => !loadOperation.Entities.Contains(entity)).ToList();
results.ForEach( enitity => _domainContext.Customers.Detach(entity));
}, null);
}, null);
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
The above code will after a SubmitChanges Load new entities from the server. The LoadBehavior is set to RefreshCurrent, it will make sure to refresh the current entities in the cached EntitySet, but will not remove anything. When the Load operation is completed and we got the new entities from the server, we then need to get the entities which is not part of the last Loading from the cached EnitySet:
var results = _domainContext.Customers.Where(
entity => !loadOperation.Entities.Contains(entity)).ToList();
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Then we need to Detach the entities from the EntitySet. We can’t use the Remove method of the EntitySet, it will only mark entities for deletion. But the Detach method will not mark them and just remove them form the EntityList.
results.ForEach( enitity => _domainContext.Customers.Detach(entity));
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
We could also just clear the EntitySet buy setting using its Clear method before calling the Load method, but if we have bound the EntitySet to a data-bound control,the control will be notified when the EntitySet is cleared. That will make it “blink”. With the above solution there will not be any blinking.
I will not take all the credits for the solution in this blog post, I will thank Colin Blair for the basic ideas of removing entities from the EntitySet. In the future I hope we will se some other solution with less efforts.
If you want to know when I publish a new blog post, you can follow me on twitter: http://www.twitter.com/fredrikn
|
-
Yesterday I played with the WebBrowser control shipped with Silverlight 4. I was trying to create a little RSS Reader for Silverlight 4, only to try out some of the new features. The WebBrowser control have two methods to display a web page, the Navigate and the NavigateToString. The Navigate takes an URI, the NavigateToString takes a string with for example HTML to be diplsyed inside of the WebBrowser window. In my case I didn’t want to use the Navigate, instead the NavigateToString so I could just grab some RSS feeds and get the body of a post and pass it to the WebBrowser control. First of all I notice that the WebBrowser control will only work in a Out-of-browser application, if we try to use it in a browser we will se the following:
Here is the XAML:
<UserControl x:Class="SilverlightBlog.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="300" d:DesignWidth="400">
<Grid x:Name="LayoutRoot" Background="White">
<WebBrowser x:Name="MyBrowserControl" Width="800" Height="600" />
</Grid>
</UserControl>
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
To enable out-of-browser we can right click on the Silverlight application and select properties in the menu, then under Silverlight, we have the “Enable running application out of browser” option, and also the button “Out-of-Browser Settings”, to set up the out-of-browser application.
Note: I have selected the “Require elevated trust when running outside the browser” checkbox. The reason to this is to be able to use the WebClient class or the WebBrowser control to navigate to any URL. If the checkbox is not selected, we can only open pages located on our own server.
With the “Require elevated trust when running outside the browser”, I can now easy use the WebClient to request a URL anywhere in the cloud:
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
if (App.Current.IsRunningOutOfBrowser)
{
var blogRequest = new WebClient();
blogRequest.DownloadStringCompleted += blogRequest_DownloadStringCompleted;
blogRequest.DownloadStringAsync(
new Uri("http://weblogs.asp.net/fredriknormen/rss.aspx", UriKind.Absolute));
}
}
void blogRequest_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
{
MyBrowserControl.NavigateToString(e.Result);
}
}
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
In the code I will first check if the application is currently running out-of-browser. It not, there are no reason to try to request a URL and then add the response to the WebBrowser control. As you can see I take the RSS returned from my blog and added it to the NavigateToString method of the WebBrowser. This will only show a lot of text. I only wanted to see if I could get my RSS feed. What I was planning to do is to use the XmlReader to only get the blog post content which includes HTML and pass it to the NavigateToString method. During the implementation of this example, I wanted to debug the out-of browser application. One way to do it is by attaching to the out-of-browser launcher, but there is another way we can do it in VS 2010. First we need to install our application as out-of-browser:
Note: When running in a elevated trust we will get another installation window. You will also notice that we now have a timer on the Install button when we install a out-of-browser app. So we need to wait about 3 seconds before we can install an app. It will make sure people need to pay more attention to what the window are displaying.
Now when the application is up an running we can close it, then go back to VS 2010 and go to the property window for our Silverlight project, select the Debug tab and the “Installed out-of-browser application”. Make sure your Silverlight project is not the start-up project. Then just start debugging. I love this feature!
If you want to know when I publish more posts to my blog, you can follow me on twitter: http://www.twitter.com/fredrikn
|
|
|
|