In this part, I will continue the implementation of the presentation tier for Windows Mobile, and if you want some background, please see the previous parts:
Part 1 was a general introduction Part 2 outlined the architecture (tiers, etc) Part 3 showed the benefit of loosely coupled tiers (distribution, cloud, etc) Part 4 started the implementation by creating the entity data model (using ADO.NET Entity Framework) Part 5 published the entity data model as a data service (using ADO.NET Data Services) Part 6 implemented the business domain (using the data service) Part 7 created the service (using WCF) Part 8 started the implementation of the mobile client (using WCF) The implemented architecture is published on CodePlex in a project called KISS Architecture, and this means that you can access the full source code as well as discuss it, come with suggested improvements, etc. As I walk you through the creation of the architecture, I suggest you keep the source code handy to check out more details.
In the previous part (8) of this blog series, I showed how the Windows Mobile client can call a WCF service when connected to the network. However, there are a number of times when the mobile device is not connected and still need to allow the application to function. It would be really awesome if the new technologies like ADO.NET Data Services, ADO.NET Entity Framework, LINQ to Entities, etc, were available also for Windows Mobile and .NET Compact Framework, as this would mean that we could use identical code (assemblies/tiers) on the client, but unfortunately they are not (yet).
So, what is the second best solution? I still want to make use of the entities defined by the (data) service tier, and not maintain any custom entity code. Also, I want the implementation to be as simple as possible (remember the KISS principle) and make it as easy as possible to upgrade when these new technologies are available. That means that I don't want to spend time writing my own custom framework, but rather something simple that just works.
To allow local data storage, I create a SQL Server Compact database by adding a "Database File" named Northwind.sdf to the Kiss.Mobile project. To that database, I then add the same tables that our order (data) service defines (see part 4 of this blog series). I also add a plain class named Common.cs to the same project that is a singleton holding a single connection to the database.
With the local database in place and a connection to it, the code for the "Get" menu item can be changed into:
private void getMenuItem_Click(object sender, EventArgs e)
{
Customer[] customers;
if(online)
{
OrderServiceClient.EndpointAddress = new EndpointAddress("http://192.168.0.100:2222/OrderService.svc");
OrderServiceClient service = new OrderServiceClient();
customers = service.GetCustomersByCity(cityTextBox.Text);
}
else
{
OrderHandler data = new OrderHandler();
customers = data.GetCustomersByCity(cityTextBox.Text);
}
dataGrid.DataSource = customers;
}
The variable indicating connection state (online) can be either manual or automatic through SystemState.ConnectionsCount. Before we take a look at the OrderHandler class, here's the updated code for the "Update" menu item:
private void updateMenuItem_Click(object sender, EventArgs e)
{
Customer customer = ((Customer[])dataGrid.DataSource)[0];
customer.City = customer.City + "X";
if(online)
{
OrderServiceClient service = new OrderServiceClient();
service.UpdateCustomer(customer);
}
else
{
OrderHandler data = new OrderHandler();
data.UpdateCustomer(customer);
}
}
Ok, so with that in place, here's the implementation of the OrderHandler class:
public class OrderHandler
{
public Customer[] GetCustomersByCity(string city)
{
//var q = from c in data.CustomerSet
// where c.City.Contains(city)
// select c;
return HandlerHelper.GetObjects<Customer>(string.Format(
"SELECT * FROM Customers WHERE City LIKE '%{0}%'", city));
}
public void UpdateCustomer(Customer customer)
{
HandlerHelper.UpdateObject<Customer>(customer, string.Format(
"SELECT * FROM Customers WHERE CustomerID='{0}'", customer.CustomerID));
}
}
Not much happening here except that the handler make use of a helper that I wrote to take care of the plumbing. I can use the entity type and I need to define the query as plain SQL, but note that I've kept the desired LINQ as a comment in preparation for the day when it can be used. Let's look at the implementation of the helper class:
public static class HandlerHelper
{
public static T[] GetObjects<T>(string sql) where T : new()
{
List<T> objects = new List<T>();
SqlCeCommand command = Common.Values.DatabaseConnection.CreateCommand();
command.CommandText = sql;
SqlCeDataReader reader = command.ExecuteReader();
Type type = typeof(T);
while(reader.Read())
{
T entity = new T();
for(int i = 0; i < reader.FieldCount; i++)
{
PropertyInfo propertyInfo = type.GetProperty(reader.GetName(i));
propertyInfo.SetValue(entity, reader[i], null);
}
objects.Add(entity);
}
return objects.ToArray();
}
public static void UpdateObject<T>(T entity, string sql)
{
SqlCeCommand command = Common.Values.DatabaseConnection.CreateCommand();
command.CommandText = sql;
SqlCeResultSet resultSet = command.ExecuteResultSet(ResultSetOptions.Updatable);
resultSet.Read();
Type type = typeof(T);
for(int i = 0; i < resultSet.FieldCount; i++)
{
PropertyInfo propertyInfo = type.GetProperty(resultSet.GetName(i));
object value = propertyInfo.GetValue(entity, null);
if(!value.Equals(resultSet.GetValue(i)))
resultSet.SetValue(i, value);
}
resultSet.Update();
}
}
The database connection kept by the singleton (Common.Values) is used to either query or update the local database. Both methods are implemented using generics, and reflection is used to find all the attributes (fields). To optimize performance, a data reader is used for the query and a resultset is used for the update. Both methods are somewhat simplified as some extra null handling is necessary but does not add to the discussion. Using reflection is not an ideal solution, but remember that my goal is to keep the implementation as simple as possible and also that this is not a solution for the future, it's a temporary fix in the lack of the right technologies on the device. When that day comes, the helper class can be scrapped, and the handler class either updated or even replaced by something like LINQ to Entities.
In the next part, I will conclude this series.