Sunday, April 4, 2010

MultiuseModel-View (MMV) Object modeling pattern with WPF and WCF: The Other Side



Download sample CommunicationObject File
Download sample ICommunicationServiceContract File
Download sample CommunicationCollectionEx File
Download sample CommunicationService File
Download sample ServiceClient File
Download sample NetDataContractSerializerBehavior File
Download sample NetDataContractSerializerElement File
Download sample NetDataContractSerializerOperationBehavior File



In my previous articles, I described how to create a DatabaseObject<T> that allows the inheritors to maintain a persistent state on a database. In this article I will show you how to create an object that sends itself across the wire from and to the client using WCF.



Fore note


Because of the complexity involved in serializing objects and sending them through the wire between servers or between a server and a client, we cannot simply wrap all the functionality into one class. Instead we'll have to create several classes that work together under the umbrella of a "facade" class (our CommunicationObject<T>). Moreover, much like the database functionality provided by DatabaseObject<T> and DatabaseCollectionEx, we'll have to provide both single instance and collection implementations of our communication classes.



The CommunicationObject<T> Class



The CommunicationObject<T> is a very simple class, since by itself it doesn't do much other than relying on other classes to send itself to or get itself from the server. Much like the DatabaseObject<T>, the CommunicationObject<T> provides only two methods: LoadFromServer and SaveToServer.


public abstract class CommunicationObject : DatabaseObject
{
    public void LoadFromServer(object primaryKeyValue)
    {
        ServiceClient client = new ServiceClient();
        object loaded = client.ContractChannel.LoadObject(typeof(T), primaryKeyValue);
        foreach (PropertyInfo pi in typeof(T).GetProperties())
            pi.SetValue(this, pi.GetValue(loaded, null), null);
    }

    public void SaveToServer()
    {
        ServiceClient client = new ServiceClient();
        // We call CreateOverridenType first to ensure that the server has loaded the extended type of T on the dynamic assembly.
        // Failure to do so would result in a CommunicationException because the deserializer would not be able to find the type.
        client.ContractChannel.CreateOverridenType(typeof(T));

        object reloaded = client.ContractChannel.SaveObject(this);
        foreach (PropertyInfo pi in typeof(T).GetProperties())
            pi.SetValue(this, pi.GetValue(reloaded, null), null);
    }
}

What's important to note here is that these methods themselves are the ones who instantiate a service client. In other words, we are wrapping the functionality that typically lives elsewhere in our applications and usually is spread around in multiple places into just one class that any of the business objects can inherit from. Other than that, the CommunicationObject ensures that the object instance properties are updated every time an object is loaded or saved.


The ServiceClient uses the ICommunicationService interface as its contract. The ICommunicationService interface defines methods for both single instance objects as well as collections and it also offers the ability for the caller to create an overridden type remotely (this is important to ensure that both the client and the server have the same types loaded).


[ServiceContract]
public interface ICommunicationServiceContract
{
    // CommunicationObject Methods

    [OperationContract]
    void CreateOverridenType(Type databaseObjectType);

    [OperationContract]
    object LoadObject(Type databaseObjectType, object primaryKeyValue);

    [OperationContract]
    object SaveObject(object objectToSave);

    // CommunciationCollection Methods

    [OperationContract]
    object LoadCollection(Type databaseObjectType);

    [OperationContract]
    object LoadCollectionWithCriteria(Type databaseObjectType, ICriterion[] criteria);

    [OperationContract]
    object SaveCollection(object collectionToSave);

}


CommunicationCollectionEx


Thanks to extension methods in .Net 3.5 we don't have to create our own collection class to extend the functionality of each type of collection (List, Dictionary, etc). Instead we can just use a bunch of extension methods that operate on ICollection<T> to provide the functionality we want.


public static class CommunicationCollectionEx
{
    public static void LoadFromServer(this ICollection collection)
    {
        // we ensure that the returning objects have a corresponding type loaded.
        ObservableObject.CreateOverridenType();

        ServiceClient client = new ServiceClient();
        object loaded = client.ContractChannel.LoadCollection(typeof(T));
        foreach (T item in (ICollection)loaded)
            collection.Add(item);
    }

    public static void LoadFromServer(this ICollection collection, params ICriterion[] criteria)
    {
        // we ensure that the returning objects have a corresponding type loaded.
        ObservableObject.CreateOverridenType();

        ServiceClient client = new ServiceClient();
        object loaded = client.ContractChannel.LoadCollectionWithCriteria(typeof(T), criteria);
        foreach (T item in (ICollection)loaded)
            collection.Add(item);
    }

    public static void SaveToServer(this ICollection collection)
    {
        // we ensure that the returning objects have a corresponding type loaded.
        ObservableObject.CreateOverridenType();

        ServiceClient client = new ServiceClient();
        // We also ensure that the object has been overriden on the server as well.
        client.ContractChannel.CreateOverridenType(typeof(T));
        object loaded = client.ContractChannel.SaveCollection(collection);
        collection.Clear();
        foreach (T item in (ICollection)loaded)
            collection.Add(item);
    }
}

As you can see, there are also two basic methods (one is overloaded) defined in this class: LoadFromServer and SaveToServer. Much like their single-instance counterparts, these methods create an instance of SeviceClient to call the appropriate operations defined by ICommunicationServiceContract. The overloaded LoadFromDatabase object also allows for defining a set of criteria to reduce the resultset.



Implementing CommunicationService


Previously I talked about the ICommunicationServiceContract as the provider of functionality to get and save objects across the wire. This interface needs an implementation, and for reusability purposes we want to define it as part of our core classes.


public class CommunicationService : ICommunicationServiceContract
{
    public void CreateOverridenType(Type databaseObjectType)
    {
        ObservableObject.CreateOverridenType(databaseObjectType);
    }

    public object LoadObject(Type databaseObjectType, object primaryKeyValue)
    {
        object databaseObject = Activator.CreateInstance(ObservableObject.CreateOverridenType(databaseObjectType));
        databaseObjectType.GetMethod("LoadFromDB").Invoke(databaseObject, new object[] { primaryKeyValue });
        return (databaseObject);
    }

    public object SaveObject(object objectToSave)
    {
        objectToSave.GetType().GetMethod("SaveToDB").Invoke(objectToSave, null);
        return (objectToSave);
    }

    public object LoadCollection(Type databaseObjectType)
    {
        object list = Activator.CreateInstance(typeof(List<>).MakeGenericType(databaseObjectType));
        MethodInfo loadFromDBInfo = (from mi in typeof(DatabaseCollectionEx).GetMethods() where mi.Name == "LoadFromDB" && mi.GetParameters().Length == 1 select mi).First().MakeGenericMethod(databaseObjectType);
        loadFromDBInfo.Invoke(null, new object[] { list }); ;
        return (list);
    }

    public object LoadCollectionWithCriteria(Type databaseObjectType, ICriterion[] criteria)
    {
        object list = Activator.CreateInstance(typeof(List<>).MakeGenericType(databaseObjectType));
        MethodInfo loadFromDBInfo = (from mi in typeof(DatabaseCollectionEx).GetMethods() where mi.Name == "LoadFromDB" && mi.GetParameters().Length == 2 select mi).First().MakeGenericMethod(databaseObjectType);
        loadFromDBInfo.Invoke(null, new object[] { list, criteria }); ;
        return (list);
    }

    public object SaveCollection(object collectionToSave)
    {
        MethodInfo saveToDBInfo = (from mi in typeof(DatabaseCollectionEx).GetMethods() where mi.Name == "SaveToDB" && mi.GetParameters().Length == 1 select mi).First().MakeGenericMethod(collectionToSave.GetType().GetGenericArguments()[0]);
        saveToDBInfo.Invoke(null, new object[] { collectionToSave }); ;
        return (collectionToSave);
    }
}

There isn't much of a mystery about this class. It is fairly simple. By using reflection we invoke the appropiate DatabaseObject<T> or DatabaseCollectionEx method to get or save data to the database.



Helper Classes


There are a number of supporting classes that we need to make the classes described above work properly with WCF. The first one being the ServiceClient class we used in our CommunicationObject and CommunicationCollectionEx classes. This class is very simple since all it does is extend the System.ServiceModel.ClientBase class and provides a public accessor to the Channel casted to ICommunicationServiceContract.


internal sealed class ServiceClient : System.ServiceModel.ClientBase where T : class
{
    public T ContractChannel
    {
        get { return ((T)Channel); }
    }

    public ServiceClient()
    {
    }

    public ServiceClient(string endpointConfigurationName) :
        base(endpointConfigurationName)
    {
    }

    public ServiceClient(string endpointConfigurationName, string remoteAddress) :
        base(endpointConfigurationName, remoteAddress)
    {
    }

    public ServiceClient(string endpointConfigurationName, EndpointAddress remoteAddress) :
        base(endpointConfigurationName, remoteAddress)
    {
    }

    public ServiceClient(Binding binding, EndpointAddress remoteAddress) :
        base(binding, remoteAddress)
    {
    }
}

Also, other important helper classes are the NetDataContractSerializerX set of classes. These being the NetDataContractSerializerElement, NetDataContractSerializerBehavior and NetDataContractSerializerOperationBehavior which are in charge of serializing objects using the NetDataContractSerializer instead of the traditional DataContractSerializer.


public class NetDataContractSerializerOperationBehavior : DataContractSerializerOperationBehavior
{
    public NetDataContractSerializerOperationBehavior(OperationDescription operationDescription)
        : base(operationDescription)
    {
    }
    public override XmlObjectSerializer CreateSerializer(Type type, string name, string ns, IList knownTypes)
    {
        return new NetDataContractSerializer();
    }
    public override XmlObjectSerializer CreateSerializer(Type type, XmlDictionaryString name, XmlDictionaryString ns, IList knownTypes)
    {
        return new NetDataContractSerializer();
    }
}

public class NetDataContractSerializerBehavior : Attribute, IServiceBehavior, IEndpointBehavior
{
    public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
    {
    }

    public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection endpoints, BindingParameterCollection bindingParameters)
    {
    }

    public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
    {
        foreach (var endpoint in serviceDescription.Endpoints)
            this.RegisterContract(endpoint);
    }

    public void Validate(ServiceEndpoint endpoint)
    {
    }

    public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
    {
        this.RegisterContract(endpoint);
    }

    public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
    {
    }

    public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
    {
    }

    protected void RegisterContract(ServiceEndpoint endpoint)
    {
        foreach (OperationDescription desc in endpoint.Contract.Operations)
        {
            var dcsOperationBehavior = desc.Behaviors.Find();
            if (dcsOperationBehavior != null)
            {
                int idx = desc.Behaviors.IndexOf(dcsOperationBehavior);
                desc.Behaviors.Remove(dcsOperationBehavior);
                desc.Behaviors.Insert(idx, new NetDataContractSerializerOperationBehavior(desc));
            }
        }
    }
}

public class NetDataContractSerializerElement : BehaviorExtensionElement
{
    public override Type BehaviorType
    {
        get { return typeof(NetDataContractSerializerBehavior); }
    }

    protected override object CreateBehavior()
    {
        return new NetDataContractSerializerBehavior();
    }
}

With all these things in place, all we need to do is ensure that these assemblies are referenced by both our client and our server projects. If so, calling the "SaveToServer" or "GetFromServer" methods of a class that inherits CommunicationObject<T> should yield the expected results.


In my next article, I'll demonstrate how to extend client-server functionality to allow for multi-tier services and how to put everything together into a nice MultiuseObject<T>.