Friday, January 29, 2010

Using DataForm and stored procedures in Silverlight

The DataForm in Silverlight is a tremendously powerful tool for allowing users to do CRUD (Create, Read, Update, Delete) operations on an object (data). It is very much like Dyanmic Data in ASP.NET. It takes all the tedious binding, validation, UI layout, etc off your plate and makes it fast and easy to do create a CRUD interface for an entity. Below is not everything you would need, but it is enough to get you going.

Create a Silverlight solution

If you have not done so already you should have a Silverlight solution. The solution should have two parts. The first is the Silverlight application itself. The other is a web site that will host the silverlight application and the WCF service or web service.

Create an Entity

In our example, we are working with Companies, so we will create a company entity. This will be used on the server and client side. When you consume your web service in your Silverlight application it will bring in your Company Entity and create a very nice Company object for use in your Silverlight application.

Below is an example of what the Company entity might look like

public class Company
{
public string Name {set;get;}
public string Phone {set;get;}
public string Region {set;get;}
}

Create your Data Access Layer

You can create your data access layer anyway you want. Below is approximately what your DAL should have in it. The important thing is that you define the CRUD operations that your web service or WCF service will call. In this example, I elude to using stored procedures, but I assume you know how to implement stored procedures and read and fill objects from/to Company Objects.

public class DAL
{

public int AddCompany(Company company)
{
    // call stored proc here
}

public void UpdateCompany(Company company)
{
    // call stored proc here
}

public Company GetCompanyByID(int CompanyID)
{
    // call stored proc here
}

public List<Company> GetAllCompany()
{
    // call stored proc here
}

public void DeleteCompanyByID(int CompanyID)
{
    // call stored proc here
}

}

 

Create a web service or WCF service

Since your Silverlight application will run in a browser on the end user’s pc we cannot connect directly to the database for security restrictions (good ones) that Silverlight imposes, we need to have a web service or WCF service to get data. WCF requires a bit more explanation and setup, but web services are a bit easier to talk about and more people know about them so I will use a web service in this example. You will need CRUD operations defined. Here are some that I use for my Company Entity.

[WebMethod]
public int AddCompany(Company company)
{
    DAL dal = new DAL();
    return dal.AddCompany(company);
}

[WebMethod]
public void UpdateCompany(Company company)
{
    DAL dal = new DAL();
    dal.UpdateCompany(company);
}

[WebMethod]
public Company GetCompanyByID(int CompanyID)
{
    DAL dal = new DAL();
    return dal.GetCompanyByID(CompanyID);
}

[WebMethod]
public List<Company> GetAllCompany()
{
    DAL dal = new DAL();
    return dal.GetAllCompany();
}

[WebMethod]
public void DeleteCompanyByID(int CompanyID)
{
    DAL dal = new DAL();
    dal.DeleteCompanyByID(CompanyID);
}

Add DataForm

The easiest way to add a DataForm to your Silverlight application is to use Blend Expression. Simply drag it from your Assets to the layout. The name space will automatically be added. I have added a couple of event handlers (You add them like you would in ASP.NET by double-clicking the event on the event tab of the property tab in Expression Blend or just manually typing them in) that we will use later. I have also set the last three properties the way that I like them or at least like to think about how I want them. The most important part is that the ItemsSource property is set as shown below (at least for this example).

<dataFormToolkit:DataForm
x:Name="dfCompany"
ItemsSource="{Binding}"
AddingNewItem="dfCompany_AddingNewItem"
EditEnded="dfCompany_EditEnded"
CommandButtonsVisibility="All"
AutoCommit="False"
AutoEdit="False"
>

Databinding

The data will be coming from our web service or WCF service. We need to call the web service (or WCF service). In this example, the DataForm is a control that I put in a ChildWindow (popup window). The constructor takes the company id as a parameter. If it is specified then I show the record in the DataForm, otherwise, I assume we are adding a new company.

It is important to realize that all the calls to the web service will be asynchronous so you need to have event handlers that get called when the call is done. This is when/where you check for exceptions and do something with the data. That is why there are so many event handlers.

One of the most important things is the constructor. We bind our results to the DataContext of the ChildWindow. This means when we set the ItemsSource property on our DataForm earlier to {Binding} that means that the DataContext and the ItemsSource will be linked with a two-way databinding.

NOTE: You should NOT bind the ItemsSource (and thus the DataContext in this case) to a single object. It should be bound to a collection of objects even if there is only one object in that collection. Otherwise you will not get the intended result. The DataForm will most likely be blank with no fields on it. In our example, we are only binding to one object that is wrapped in a collection. However, the DataForm supports previous, next, first, and last operations if you pass it more than one record. That may be very useful depending on your application. Try calling the GetAllCompany method instead of GetCompanyByID (and don’t wrap it in a collection since it is already) and you will see this very cool functionality.

I added an adding flag also. This allows me to call either the Add or Update method on our web service when the Save button is called. There are other ways of doing this, but this is pretty straight forward.

public partial class CompanyEditor : ChildWindow
{
bool adding = false;

Example.CompanyWS.CompanyWSSoapClient db;

public CompanyEditor(int? companyID)
{
InitializeComponent();

db = new Example.CompanyWS.CompanyWSSoapClient();
db.GetCompanyByIDCompleted += new EventHandler<Example.CompanyWS.GetCompanyByIDCompletedEventArgs>(db_GetCompanyByIDCompleted);
db.AddCompanyCompleted += new EventHandler<AddCompanyCompletedEventArgs>(db_AddCompanyCompleted);
db.UpdateCompanyCompleted += new EventHandler<System.ComponentModel.AsyncCompletedEventArgs>(db_UpdateCompanyCompleted);

if (companyID.HasValue)
{
db.GetCompanyByIDAsync(companyID.Value);
}
else
{
List<Company> newOne = new List<Company>();
newOne.Add(new Company());
DataContext = newOne;
}
}



void db_GetCompanyByIDCompleted(object sender, Example.CompanyWS.GetCompanyByIDCompletedEventArgs e)
{
if (e.Error == null)
{
List<Company> results = new List<Company>();
results.Add(e.Result);
DataContext = results;

}
else
{
MessageBox.Show("Error loading record");
}
}

private void OKButton_Click(object sender, RoutedEventArgs e)
{
this.DialogResult = true;
}

private void CancelButton_Click(object sender, RoutedEventArgs e)
{
this.DialogResult = false;
}

private void dfCompany_EditEnded(object sender, System.Windows.Controls.DataFormEditEndedEventArgs e)
{
Company company = ((DataForm)sender).CurrentItem as Company;

if (e.EditAction == DataFormEditAction.Commit)
{
if (adding)
{
db.AddCompanyAsync(company);
}
else
{
db.UpdateCompanyAsync(company);
}

}
else if (e.EditAction == DataFormEditAction.Cancel)
{

}
else
throw new Exception("Unknown action: " + e.EditAction);

adding = false;

}

void db_UpdateCompanyCompleted(object sender, System.ComponentModel.AsyncCompletedEventArgs e)
{
if (e.Error != null)
{
MessageBox.Show("Error Saving. The error has been logged. Please try again.");
}
}

void db_AddCompanyCompleted(object sender, AddCompanyCompletedEventArgs e)
{
if (e.Error != null)
{
MessageBox.Show("Error Saving. The error has been logged. Please try again.");
}
}


private void dfCompany_AddingNewItem(object sender, System.Windows.Controls.DataFormAddingNewItemEventArgs e)
{
adding = true;
}
}

Controlling the DataForm

The DataForm knows what to display by looking at the entities you pass it at runtime. In fact, if you pass it a collection that contains different types of objects it will change the User Interface as you move through each record. Very impressive!

So where does it get all this information? It does so by looking at the annotations on each public property of the entity that you pass it. Since the object that we pass it is generated automatically from the web service we consume we don’t really have much chance to add annotations to it (at least not very easily).

It is recommended to reduce server roundtrips, and improve user experience that validation be done on the client and server when possible. This means in Silverlight and in our web service. I don’t know if I totally agree with that or not. I see the need for both, but it seems like we are back to the same issue we had with ASP.NET and that is writing the business logic twice. If we are careful about what gets duplicated I think it can be a great benefit. I try to limit the business logic that is duplicated to be just annotations, or at least use the ones that are only available in Silverlight.

One way to do this is to wrap all our entities in another entity and add the annotations to that wrapping entity. That way the DataForm can be controlled much better. Dino Esposito talks about this here. This wrapping can be time consuming and a maintenance nightmare, so use only when appropriate.

Working with multiple table

It seems to be a fairly common question on the internet. The question is, the DataForm works great for an entity that maps to a single table, but how to I use this when I have multiple tables. The answer is quite simple if we follow the design presented here. Whether there is one or two tables is not a UI decision in fact, it is not even at the business logic level, it is solely at the data access or database level. Please note, I did not implement the business logic layer by the way since this was such a simple example, but you may consider putting it between the web service and the data access layer. In that case, the web service would call the business logic layer which would call the data access layer.

I am happy to say that this design allows for this quite nicely. The change does not take place in the UI or Business Logic layer, but instead the change is at the data access layer. You could do it at the business logic layer or in the web service if you want also, but that is entirely up to you. The key is that interface between the web service and the Silverlight application doesn’t change just because columns are split between two or more tables instead of being in one table. We encapsulate that in the web service (or some layer below it) and the Silverlight application and thus the DataForm don’t know the difference. In my opinion, that is how it should be, or at least a reasonable approach, I’m sure there are better and definitely different solutions. I would be interested to hear of different approaches.

No comments: