2009-07-27

Mvc for Winforms - Mapping the View event to the Controller action Part III

Long time since part I and II were posted, but I've been quite busy lately so I haven't found time (or prioritized) to write the last part, but here we are :)

To recap, in part I I explained about the background of the issue, that I was writing a small winforms MVC framework. The goal of the framework was to be able to instruct the controller to listen to a component in the view and when the component called an event, a specific method would be called.
For example, the call below would register the click event from the saveButton
Controller.RegisterAction(saveButton,"Save");
(in later editions I have also added support for lamda expressions instead of method names, and thus almost abandoned the previous)
Controller.RegisterAction(rotateRightButton,() => Controller.Rotate(90));
Controller.RegisterAction(rotateLeftButton,() => Controller.Rotate(-90));
In part II I wrote about how find and listen to the event of the component.
In this section I will explain how I call the method.

What we ended with in the last post was that we had registered the controllers event to call the ExecuteAction method of the Controller class.
public void ExecuteAction(object source, object eventArgs, ActionData actionData)
or in the latter case using lambda expressions (or action)
public void ExecuteAction(object source, object eventArgs, Action action)
In this case the Action is the easiest to implement. Just call the Invoke method of the Action instance and you are done.
public void ExecuteAction(object source, object eventArgs, Action action) {
action.Invoke();
}
The no Action ExecuteAction method (almost abandoned way )
The ActionData variant is somewhat more tedious but this is how ASP.NET MVC does it.
First we need to find the method to call. The ActionData contains the name of the method. We now needs to find the method of the controller that matches that name. MVC for ASP.NET uses the ActionSelectorClass. The ActionSelectorClass is responsible for retrieving the MethodInfo for an action by passing in the name of the action. It support action aliases and other things so look it up for a great example.
In short it uses reflection to get all methods, filters out irrelevant methods and returns the one that matches the name.
Example (really shortened example, lookup ActionSelectorClass in the ASP.NET MVC framework for a complete example.)
//get all methods
MethodInfo[] array = ControllerType.GetMethods(BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Instance);
//convert to a dictionary
ILookup<string,MethodInfo> methodLookup = array.ToLookup(method => method.Name, StringComparer.OrdinalIgnoreCase);
//return the matching MethodInfo
MethodInfo action = methodLookup[actionName];
Now we have the MethodInfo to invoke, only to calculate the parameters to pass to the method is left.

The action is invoked using the Invoke method of the MethodInfo class and we need to pass the instance to invoke it on (the controller instance) and the object array containing the parameters to pass to the method/action.
Example
action.Invoke(controller,parameterValues);
If the action don't have any parameters we just pass null, but if we do have parameters we need to assign values for them.
Remember: we wanted to be able to register the call
Controller.RegisterAction(createBoldTextButton, "CreateText", new {name="Bold", type=4}); 
To call the action method
public void CreateText(string name, int type) {...}  
using the "Bold" as value for the name parameter and 4 as value for the type parameter.

To succeed with this we need to do some parsing.
  1. Iterate all parameters of the method info.
  2. Handle special cases (like if the parameter is named source and is of type object, then the source value from the event handler method should be passed).
  3. Find a property with a matching name from the value object passed in the RegisterAction.
Iterate all parameters of the method info
Using reflection it's easy to find the parameters.

List<object> objects = new List<object>();
foreach (ParameterInfo info in method.GetParameters()) {
objects.Add(GetParameterValue(info));
}
return objects.ToArray();
Handle special cases
The GetParameterValue method will return the value to pass to the supplied ParameterInfo. In some cases the value to be passed as parameter is not any of the values passed to the values parameter (in the RegisterAction method), like for example, if one would need to get the EventArgs or source parameter passed from the event component, then we would need to handle them. (other customizations can be done here, this is an example)
Type parameterType = parameterInfo.ParameterType;
string parameterName = parameterInfo.Name;

//handle eventArgs and source parameters
if (string.Equals(parameterName, "e", StringComparison.OrdinalIgnoreCase) && _eventArgs != null && parameterType.IsAssignableFrom(_eventArgs.GetType()))
return _eventArgs;
if (string.Equals(parameterName, "source", StringComparison.OrdinalIgnoreCase)&& parameterType.Equals(typeof(object)))
return _source;

//parse the values object
return GetValuesValue(parameterType, parameterName);
Find a matching property of the value object
At last we would try to find a matching property of the value object that matches the parameter type and name. (First we check that the parameter don't matches the whole value object)
//if we don't have a value, return null
if (ValueType == null)
return null;
//if the value is matching, use it as value
if (type.Equals(ValueType))
return _actionData.Values;
PropertyInfo[] valueProperties= ValueType.GetProperties();
//if the value don't have any properties, return null
if (valueProperties.Length == 0)
return null;
//find matching property
PropertyInfo[] possibleMatches = valueProperties
.Where(p => string.Equals(name, p.Name, StringComparison.OrdinalIgnoreCase)
&& type.IsAssignableFrom(p.PropertyType))
.ToArray();
if (possibleMatches == null || possibleMatches.Length == 0)
return null;
if (possibleMatches.Length > 1)
throw new AmbiguousMatchException(string.Format("The value collection contains ambigous match values for the parameter {0} {1}", type, name));
return possibleMatches[0].GetValue(_actionData.Values, null);
Ok, so now we can invoke the method with our array of parameters. (as I said, a bit tedious).
If we compare the two variants,
Controller.RegisterAction(createBoldTextButton, "CreateText", new {name="Bold", type=4});
Controller.RegisterAction(createBoldTextButton, ()=> Controller.CreateText("Bold", 4);
you understand why I have abandoned this last variant and only uses Actions/lambda functions.
The tedious method can still be used if you want to lift the connection between view and controller to a configuration layer. Then the Action method will be hard to implement.

That sums up this last part of my WinForms MVC framework lessons and experiences. Hope that you learned something.