Growing an MVVM Framework in 2003, part I—Event Handlers
At the Day Job I usually work on web services, but I recently had the opportunity to write a customer-facing tool that had a GUI.
Previously, I expressed my excitement over the Rob Eisenberg "Build Your Own MVVM framework" talk. Ever since, I've been dying to try my hand at an MVVM application. I wanted to create an application that
- had testable logic, even in the GUI layer,
- had no "codebehind" in the view, and
- shunted the tedious wiring up of events and handlers into helpers (or a "framework")
Unfortunately, the application was intended to work at our established customers' sites, so I couldn't depend on WPF, or even .NET 2.0—it's 1.1 all the way.
The Goal
I'll demonstrate with a simpler app than the one from work, but will cover the the relevant concepts. For the purpose of this post, I'll be writing a book-finding app. The user will be able to enter a substring to use to search a database; the matching entries will be displayed in a ListBox and when one of them is selected, some notes will be displayed in a TextBox.
I didn't want to have to riddle my ViewModel with +=
s just to be able to react to button presses and item selections from the view. I wanted to write something like:
public void FindClick(object sender, EventArgs e)
{
ICollection books = bookDepository.Find(TitleText);
BookListItems.Clear();
foreach ( string book in books )
{
BookListItems.Add(book);
}
}
and have the method run when the Click
event on the Find
button was raised. The method should use the value of the Text
property of the Title
TextBox to find a list of books and put them in the Items
collection on the BookList
ListBox.
Wiring up Event Handlers
I created a ViewModelBase class to handle all the infrastructure, so the BookListViewModel code could focus on app-related functions. The first thing ViewModelBase.BindToView
does is seek out event handlers to bind to on the supplied View (which can be any Controller object):
ArrayList allControls = AllControlsDescendingFrom(View);
foreach ( MethodInfo handler in EventHandlers() )
{
FindEventToListenTo(allControls, handler);
}
AllControlsDescendingFrom
recursively looks through all the controls rooted at the View and returns them as a flat list. EventHandlers
uses reflection to locate public methods on the ViewModel that have event-like signatures:
private IEnumerable EventHandlers()
{
ArrayList eventHandlers = new ArrayList();
foreach ( MethodInfo method in this.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public) )
{
if ( isEventHandler(method) )
{
eventHandlers.Add(method);
}
}
return eventHandlers;
}
private bool isEventHandler(MethodInfo info)
{
ParameterInfo[] parameters = info.GetParameters();
return
(info.ReturnType == typeof (void) &&
parameters.Length == 2 &&
parameters[0].ParameterType == typeof(object) &&
(typeof(EventArgs)).IsAssignableFrom(parameters[1].ParameterType));
}
Note the last line. I'd originally just checked that the second parameter was of type EventArgs
. This worked for many event types, like the Click event on a Button and the SelectedIndexChanged event on a ListBox, but failed to match others, such as a TextBox's KeyPress event:
public delegate void KeyPressEventHandler(object sender, KeyPressEventArgs e)
FindEventToListenTo
looks through the allControls list. If there's a control with name Controlname and an event Eventname, it will bind to a handler named ControlnameEventname. For example method SearchClick would be hooked up to the Click event on a control called Search.
private void FindEventToListenTo(ArrayList allControls, MethodInfo handler)
{
foreach ( Control control in allControls )
{
if ( ListenToEvent(control, handler) )
{
return;
}
}
}
private bool ListenToEvent(Control control, MethodInfo method)
{
string eventName = ControlAttributeName(control, method);
if ( eventName == null )
{
return false;
}
EventInfo eventInfo = control.GetType().GetEvent(eventName, BindingFlags.Instance | BindingFlags.Public);
if ( eventInfo == null )
{
return false;
}
eventInfo.GetAddMethod().Invoke(control, new object[]
{
Delegate.CreateDelegate(eventInfo.EventHandlerType, this, method.Name)
});
return true;
}
This is pretty straightforward, with two exceptions. Creating the delegate to wrap the ViewModel method was a little tricky—I had to reference the specific EventHandlerType
that matched the event. Similarly to the EventArgs problem above, I'd originally tried to create an EventHandler
, which failed for certain events.
The last piece is the ControlAttributeName
method, which builds the desired attribute (in this case an event) name from a control and the ViewModel member that we want to bind to. The method assumes that the name of the ViewModel member (the handler) will start with the name of the control. If there's a match, it returns the rest of the member name. Otherwise, null.
The name comparison ignores case, which wasn't necessary to hook up method handlers, but proved to be useful when wiring up properties.
private string ControlAttributeName(Control control, MemberInfo viewModelMember)
{
if ( viewModelMember.Name.ToLower().StartsWith(control.Name.ToLower()) )
{
return viewModelMember.Name.Substring(control.Name.Length);
}
return null;
}
What's next?
After wiring the event handlers, the ViewModelBase binds to the View's interesting properties. Details to follow.