Monday, December 5, 2011

Making your views thread safe, AOP style

One of the nice things of using a pattern like MVP, where the UI logic of your screens (views) is separated from the business logic of your application is that you have more flexibility to add framework level behavior, doing things right in only one place.

One common scenario we encounter in every application is the necessity to do operations in a different thread to the one of the UI, example of these operations are any that are going to be expensive, (e.g. a network/service call). Up to here all is good, but in the cases where we need to update our UI after the expensive operation finished we need to somehow marshal these calls into the UI thread.


The common way to do this is by using the Control.Invoke(Delegate method) method available in the Control class, so for example to marshal setting a property to our view, you would define it like this in your view

public class MyView: IMyView
{
    public string Message
    {
        get
        {
             Func lambda = () => tMessage.Text;
             return InvokeRequired 
                ? (string) Invoke(lambda) 
                : lambda();
        }
        set
        {
             Action lambda = () => tMessage.Text = value;
             if (InvokeRequired)
                  Invoke(lambda);
             else
                  lambda();
        }
    }
}

As I'm sure you see, this gets pretty old fast. It is a lot of code that needs to be repeated everywhere, even more it might be forgotten somewhere and hard to track back once it fails.

One way to prevent having to put this code is by doing this call in an aspect, as I'm using spring.net in our current project, I'll demonstrate it the spring way, but this can be done with any other library that supports some kind of AOP (e.g. Postsharp).

What we need to do is implement the IMethodInterceptor interface, as seen here:

public class UIThreadInterceptor: IMethodInterceptor
 {
  public object Invoke(IMethodInvocation invocation)
  {
   var control = invocation.This as Control;
   if (control == null)
   {
    Log.Warn("Aspect applied to non-control instance.");
    return invocation.Proceed();
   }

   Func<object> expression = invocation.Proceed;
   return control.InvokeRequired ? control.Invoke(expression) : expression();
  }
 }

Once we configure this, we can set our setter/getters in the normal way and the aspect will do the marshalling for us. There is one catch though, as spring.net proxies on winforms controls should be configured to work by inheritance, we need to declare the properties we want to marshal as virtual.

public class MyView: IMyView
{
    public virtual string Message
    {
        get { return tMessage.Text; }
        set { tMessage.Text = value; }
    }
}

This still is not enough, in fact, it is easier to forget the 'virtual' keyword than all the chunky code we had before, so I've created a custom test that checks that all classes implementing the IView interface, need to have all its members declared as virtual.

Test code here:
public bool TestMethodsOf(Type impl, Type @interface)
  {
   var valid = true;
   Console.WriteLine("Testing methods of {0}", impl.Name);

   foreach (var iface in impl.GetInterfaces().Where(i => @interface.IsAssignableFrom(i)))
   {
    var members = iface.GetMethods();

    foreach (var member in members)
    {
     var implMember = impl.GetMethod(member.Name, member.GetParameters().Select(c => c.ParameterType).ToArray());
     if (implMember == null)
      continue;

     if (implMember.IsVirtual && implMember.IsFinal)
     {
      Trace.WriteLine("Method " + impl.Name + "." + member.Name + " is not virtual");

      valid = false;
      continue;
     }

    }
   }
   return valid;
  }


Whoa, this post was longer than expected. Hope it helps somone!
Cheers



No comments:

Post a Comment