Convention-based programming is an interesting model. In essence, it attempts to reduce the potential for error by handling most scenarios based on conventions or standards, and allowing the developers to focus on the exceptions. Probably one of the most thorough public resources I've seen for the convention-based model is Rob Eisenberg's Build your Own MVVM Framework which he dubs "a framework with an opinion."
Click here to download the source for this example.
I've not been a huge fan or advocate of convention-based programming in the past, but that is quickly changing. What I didn't like was the secrets it keeps. In other words, the convention-based model is great, if and when you know and understand the convention. A lot of "magic" happens. I am a fan of self-documenting code, and if you aren't careful, a convention-based model might make things happen without it ever becoming clear how it happened.
I've also been taking a look at the convention-based model to gain a clearer understanding and see some definite advantages. Perhaps the most intriguing "find" has been that we are always coding by convention anyway ... it's just a question of which convention and how far we take it.
Let's take the simple example of taking text from a text block in Silverlight and sending it off to a service.
The Old-Fashioned Code-Behind Way
One possible way to do this is to simply wire an event in the code-behind and make it happen. Let's take a common scenario: enter some text, then click a button. We want the button to remain disabled until text is available, then submit the text to some service. This is the contract for the service:
public interface ISubmit { void Submit(string text); }
And our reference implementation is deadly simple:
public class SubmissionService : ISubmit { public void Submit(string text) { MessageBox.Show(text); } }
Now here is a sample control:
<Grid x:Name="LayoutRoot" Height="50" Background="White"> <StackPanel Orientation="Horizontal" Margin="5"> <TextBlock Text="Enter Some Text: " Margin="5"></TextBlock> <TextBox x:Name="TextInput" Margin="5" Width="200"/> <Button x:Name="SubmitButton" Content="Submit" Margin="5" Click="Button_Click"/> </StackPanel> </Grid>
Next, we wire it all up in the code-behind, like this:
public partial class CodeBehind { public CodeBehind() { InitializeComponent(); SubmitButton.IsEnabled = false; TextInput.TextChanged += TextInput_TextChanged; } void TextInput_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e) { SubmitButton.IsEnabled = !string.IsNullOrEmpty(TextInput.Text); } private void Button_Click(object sender, RoutedEventArgs e) { ISubmit submit = new SubmissionService(); submit.Submit(TextInput.Text); } }
That's where most people start. I've often been asked, "Jeremy, it's quick and it's simple ... what's wrong with it?" Nothing, if all you ever do is work with that single text box and single event.
The problem comes when you are composing larger applications. We lose several important aspects of productive development when our code and design is intermingled like this. For example ...
- Reusability — with the view and logic intertwined, this code can only ever do one thing. We can never reuse the view for something different (perhaps there is a different set up logic you'd want to apply to a view with a text box and a button) and we cannot re-use the business logic because it's all tied into the control.
- Extensibility — there is no real way to extend this model, we can only modify it
- Presentation Separation — this is less evident with the textbox example, but a combo box or radio button provide more clarity: what happens when I change my radio button to a check box, or my combo box to a list box? Without separation, it means I have to modify everything.
- Testability — for larger applications, catching bugs as early in the process as possible is paramount. This "thing" can only be tested one way. With a cleaner separation, we could test UI and logic separately, and isolate the fixes to one place, rather than having them impact both sides every time and possibly propagate to other controls that aren't re-using the pattern.
- Designer/Developer Workflow — this is also huge. More and more shops have a design team and a development team and their schedules don't always synchronize. With this model, you are forced to wait for design to toss something over the fence before development can even get started, and then end up in a message if elements change. Wouldn't it be nice if developers could build even before the design team was done, and the design team could toss over results without impacting what was being developed?
Those are just a few reasons I shy from that approach when dealing with large, enterprise projects that are complex and require composability, extensibility, scalability and performance (not to mention the ability for many developers to contribute simultaneously to the success of the project).
The Control Freak
One method to attempt to separate these concerns a little is the control/controller pattern. I built a few applications like this and actually preferred it for ASP.NET WebForms applications (before MVC) to help separate business concerns from control logic. In Silverlight, the pattern might look a little like this ... first, the control:
<Grid x:Name="LayoutRoot" Height="50" Background="White"> <StackPanel Orientation="Horizontal" Margin="5"> <TextBlock Text="Enter Some Text: " Margin="5"></TextBlock> <TextBox x:Name="TextInput" Margin="5" Width="200"/> <Button x:Name="ButtonSubmit" Content="Submit" Margin="5"/> </StackPanel> </Grid>
The controller contract exposes ways we interact with the controller to initialize it, have it save state, etc. For our example, we keep it simple:
public interface IController<in T> where T: UserControl { void Init(T userControl); }
Notice that our type is contravariant ... that is, we are allowing the generic to be strongly typed to the actual type of the view itself (going from generic to more specific).
This is implemented for our view like this:
public class ViewController : IController<Controller> { private readonly ISubmit _submit; private Controller _controllerView; public ViewController(ISubmit submit) { _submit = submit; } public void Init(Controller userControl) { _controllerView = userControl; userControl.ButtonSubmit.Click += ButtonSubmit_Click; userControl.ButtonSubmit.IsEnabled = _HasText(); userControl.TextInput.TextChanged += (o, e) => userControl.ButtonSubmit.IsEnabled = _HasText(); } bool _HasText() { return !string.IsNullOrEmpty(_controllerView.TextInput.Text); } void ButtonSubmit_Click(object sender, System.Windows.RoutedEventArgs e) { _submit.Submit(_controllerView.TextInput.Text); } }
In this case, we do manage to separate the logic from the view. We can even abstract marrying the controller to the view. There are different ways of doing this, but even a simple factory:
public static class ControllerFactory { public static void InitController<T>(T userControl) where T: UserControl { if (typeof(T).Equals(typeof(Controller))) { var controller = (IController<T>)new ViewController(new SubmissionService()); controller.Init(userControl); } } }
This scans the type and returns the appropriate controller, so the code-behind becomes this simple:
public partial class Controller { public Controller() { InitializeComponent(); Loaded += (o,e) => ControllerFactory.InitController(this); } }
(Another popular method is to init the controller, have it create the control and then return or inject it somewhere). While this allowed us to separate the logic from the code-behind, it is still an illusion. The controller knows too much about the view and how it is implemented. There is still tight coupling. I have to drag the view along everywhere the controller goes, so I've really just moved it into a separate file.
This pattern can be evolved with a few steps such as providing a view contract and then acting on the interface instead of the view. This will create testability, for example. Even with this extra abstraction, however, the contract ends up mirroring events and attributes on the view and sometimes might be perceived as extra work that doesn't really go far.
MVVM Comes to Town
So let's take a look at the popular pattern that I've been using in my Silverlight business applications and teaching for some time now, the Model-View-ViewModel (MVVM). I've seen people roll their eyes or cringe when MVVM is mentioned because the perception is that it can entail a lot of work. In my experience, however, the benefits far outweigh the necessary infrastructure. In fact, it like the difference between leasing and buying with a large downpayment. With MVVM, you do some heavy lifting (big downpayment) up front. Once the plumbing is in place, however, building out and extending becomes very easy (small monthly payments). Other architectures might get you to the first screen faster, but come at a maintenance and extensibility cost.
Here's a peek at the MVVM control:
<UserControl.Resources> <ViewModel:TextViewModel x:Key="VM"/> </UserControl.Resources> <Grid x:Name="LayoutRoot" DataContext="{StaticResource VM}" Height="50" Background="White"> <StackPanel Orientation="Horizontal" Margin="5"> <TextBlock Text="Enter Some Text: " Margin="5"></TextBlock> <TextBox x:Name="TextInput" Margin="5" Width="200" Text="{Binding InputText, Mode=TwoWay}"/> <Button x:Name="ButtonSubmit" Command="{Binding CommandSubmit}" CommandParameter="{Binding ElementName=TextInput,Path=Text}" Content="Submit" Margin="5"/> </StackPanel> </Grid>
The cost of MVVM is already apparent. We've introduced another dependency in the view, namely, the view model. It's nice we can create it in XAML but now we've essentially coupled the two together. Furthermore, we now have some extra markup. This is the "glue" that holds the pieces together, instructing the controls how to bind with the underlying view model.
What does that view model look like?
public class TextViewModel : INotifyPropertyChanged { public TextViewModel() { var cmd = new SubmitCommand { Submit = new SubmissionService() }; CommandSubmit = cmd; } private string _text; public string InputText { get { return _text; } set { _text = value; if (PropertyChanged == null) return; PropertyChanged(this, new PropertyChangedEventArgs("InputText")); ((SubmitCommand)CommandSubmit).RaiseCanExecuteChanged(); } } public ICommand CommandSubmit { get; set; } public event PropertyChangedEventHandler PropertyChanged; }
This is a fairly straightforward model. Instead of the business logic mixed in with our code-behind or controller, we now have a more data-centric class. What's nice is that our code doesn't deal with pulling fields from a text box or responding to events. Instead, this is all handled by the framework. We don't go after a text box control to get a string, instead, we simply deal with a string command. We don't have to know whether that string is in a text box or a password box or a custom third-party control. Heck, you can have all three and reuse the same view model. We can test the view model in isolation and because we are responding to commands, not events, we can also fire a "submit command" without screen-scraping or trying to force a mouse click.
We've witnessed that a "con" for this approach is some extra overhead and noise in the code-behind, but the benefit is that we don't have to worry about keeping track of changes in controls or moving values into or out of the UI, we just deal with properties and that "noise" is what glues them together. We can build our view model independently and test it and wire it into services and much more.
Now we come to the convention-based approach. I must admit that this appeared to me to be something a little "trendy" so I considered it with caution. My one fear of using convention-based models is that "magic" happens without really understanding it. With convention programming, the convention of how you structure and name controls can drive how they interact and behave. If you don't know the convention, this can create a bit of mystery.
One moment of epiphany for me came, however, when I was teaching the concept of data-binding to someone not familiar with Silverlight. They suddenly made it very clear that data-binding was a convention they had to learn. In this case, there is a convention of how we get the view model bound to the view. There is also a convention for data-binding itself. We must make sure we have the appropriate command, that we set the mode (one way, two way, etc.) and that the path matches the view model. So in essence, most of us are already programming with convention-based models.
So why not simplify things a bit?
Convention to the Rescue
With a convention-based engine, my control ended up looking like this:
<Grid x:Name="LayoutRoot" Height="50" Background="White"> <StackPanel Orientation="Horizontal" Margin="5"> <TextBlock Text="Enter Some Text: " Margin="5"></TextBlock> <TextBox x:Name="InputText" Margin="5" Width="200"/> <Button x:Name="Submit" Content="Submit" Margin="5"/> </StackPanel> </Grid>
Notice there is no data-binding and no view model. This will work perfectly well in Blend and the designers can modify it to their heart's content. I only ask that my convention is followed, which in this case means the UI elements are named in accordance with the view model.
First, let's look at the code-behind. I only do one small thing here (and I can easily make a behavior and do it in XAML instead): I tag the control with a convention. I'm doing a string here, but it can easily be an enumeration or something else. I call it "example":
[ControlTag("Example")] public partial class ConventionMVVM { public ConventionMVVM() { InitializeComponent(); } }
Now let's take a peek at the view model. Remember, we didn't wire in a data context or instance it in our view. In fact, there is no direct relationship between the two. When you look at the view model, you'll notice that I've create some methods and properties that match the names in the control, this is their "affinity" and is no more coupled than the path on a data-binding command. I also tag the view model with the same tag as the view:
[VMTag("Example")] public class ConventionViewModel : INotifyPropertyChanged { [Import] public ISubmit SubmitService { get; set; } private string _text; public string InputText { get { return _text; } set { _text = value; if (PropertyChanged == null) return; PropertyChanged(this, new PropertyChangedEventArgs("InputText")); PropertyChanged(this, new PropertyChangedEventArgs("CanSubmit")); } } public void Submit() { if (CanSubmit) { SubmitService.Submit(_text); } } public bool CanSubmit { get { return !string.IsNullOrEmpty(_text); } } public event PropertyChangedEventHandler PropertyChanged; }
Notice that I'm not using commands. I implement the notify property changed event and I import the submission service using the Managed Extensibility Framework so I can code against the contract. That's it!
It may seem even more puzzling when you look at my main page. I've hosted all four of the patterns represented here, but only three of the controls are referenced directly:
<Grid x:Name="LayoutRoot" Background="White"> <StackPanel Orientation="Vertical"> <Views:CodeBehind/> <Views:Controller/> <Views:MVVM/> <ContentControl x:Name="ConventionRegion"/> </StackPanel> </Grid>
Notice I don't have a reference to the view, but I do have a content control that can be filled. In fact, in the code-behind, I tag it the same way I did the view and the view model:
public partial class MainPage { [RegionTag("Example")] public ContentControl ExampleRegion { get { return ConventionRegion; } } public MainPage() { InitializeComponent(); } }
Hmmm ... so what is going on here?
Here we decided we wanted to minimize our monthly payments. We bought the car up front and put a big down payment. In the convention framework, the bulk of the work goes into the common scenarios. We build these out and isolate them to a single place where we can tweak and troubleshoot. We publish our convention and when developers follow it, things "just work." What's nice is the conventions don't meet every situation, so scenarios that aren't covered by the convention can be addressed and dealt with as one-offs. Of course, when we see a pattern emerge that is repeated, we can revisit our convention and integrate it back.
The first piece of the convention model handles gluing view models to views and populating them into regions. This is handled by a ConventionManager
class that imports and routes the pieces:
[Export] public class ConventionManager : IPartImportsSatisfiedNotification { [ImportMany] public Lazy<ContentControl, IRegionTagCapabilities>[] Regions { get; set; } [ImportMany] public Lazy<UserControl, IControlTagCapabilities>[] Views { get; set; } [ImportMany] public Lazy<Object, IVMTagCapabilities>[] ViewModels { get; set; } public void OnImportsSatisfied() { foreach(var regionImport in Regions) { var tag = regionImport.Metadata.Tag; var viewTag = (from v in Views where v.Metadata.Tag.Equals(tag) select v).FirstOrDefault(); if (viewTag == null) continue; var viewModelTag = (from vm in ViewModels where vm.Metadata.Tag.Equals(tag) select vm).FirstOrDefault(); if (viewModelTag == null) continue; var view = viewTag.Value; _Bind(view, viewModelTag.Value); regionImport.Value.Content = view; } } }
So far this piece simply routes and glues. The regions, views, and view models with similar tags are bound and routed together. Of course, this is a place where the convention engine can be easily extended. For example, your regions would most likely have panels and items controls so that multiple views could be routed through them. Views and viewmodels might have different metadata tags and other rules for marrying them together. For our simple example, this works. Now let's take a look at the _Bind
method ...
private static void _Bind(FrameworkElement view, object viewModel) { var elements = _Elements(view.FindName("LayoutRoot") as Panel).ToList(); foreach(var button in (from e in elements where e is Button select e as Button).ToList()) { var nameProperty = button.GetValue(FrameworkElement.NameProperty); if (nameProperty == null) continue; var name = nameProperty.ToString(); var actionMethod = (from m in viewModel.GetType().GetMethods() where m.Name.Equals(name) select m).FirstOrDefault(); if (actionMethod != null) { button.Click += (o, e) => actionMethod.Invoke(viewModel, new object[] {}); } var enabledProperty = (from p in viewModel.GetType().GetProperties() where p.Name.Equals("Can" + name) select p).FirstOrDefault(); if (enabledProperty == null) continue; button.IsEnabled = (bool) enabledProperty.GetGetMethod().Invoke(viewModel, new object[] {}); var button1 = button; ((INotifyPropertyChanged) viewModel) .PropertyChanged += (o, e) => { if (e.PropertyName.Equals("Can" + name)) { button1.IsEnabled = (bool) enabledProperty.GetGetMethod() .Invoke(viewModel, new object[] {}); } }; } foreach(var propertyInfo in viewModel.GetType().GetProperties()) { if (propertyInfo.GetGetMethod() == null || propertyInfo.GetSetMethod() == null) continue; var propName = propertyInfo.Name; var element = (from e in elements where propName.Equals(e.GetValue(FrameworkElement.NameProperty)) select e). FirstOrDefault(); if (element == null) continue; if (element is TextBox) { var binding = new Binding { Source = viewModel, Path = new PropertyPath(propName), Mode = BindingMode.TwoWay }; ((TextBox) element).SetBinding(TextBox.TextProperty, binding); } } } private static IEnumerable<UIElement> _Elements(Panel panel) { yield return panel; foreach(var element in panel.Children) { if (element is Panel) { foreach(var child in _Elements((Panel)element)) { yield return child; } } else { yield return element; } } }
This convention focuses on buttons and text boxes. It makes use of the iterator function to simplify recursion. The recursion of controls is done using the yield statements to flattern the discovered controls into a list. If we were searching or filtering, this would stop once the desired element was found so it provides some performance benefits as well (in this case we just blow through the whole list for the simple example).
First, we scan the buttons on the form. We are looking for a view model method with the same name as the button, and wiring the click event to invoke the method. Furthermore, if a property exists that is prefixed with "Can" and the button name, we use that to set the IsEnabled
property of the button. We could simply bind directly to a command object, but I wanted to demonstrate the flexibility of convention-based solutions and how it can end up being a simple method and property without having to introduce commands.
Next, we scan the properties on the view model. For each property, we look for an element with the same name. If it exists and is a text box, we create a binding. A fully-blown convention model would bind to password boxes, text blocks, toggle buttons (radio buttons and check boxes), combo boxes, and more.
Conclusion
What we've done with the convention model is invested some extra effort up front to handle the common scenarios (button click and text box input, as well as routing of views and view models to regions). Once this is in place, it becomes very easy to add a new view or view model. We simply tag these and then name them. We're still creating a coupling between the view and the view model, but instead of requiring a longer, complex data-binding statement, we've simplified it to a simple name and don't have to remember the binding direction or path syntax, etc.
What I really like about this model is that it truly frees the design team to move forward with minimal impact on the development workflow. In one project I've worked with, the design project is completely separate. The only "noise" is the exports of the controls, which can also be done externally. This means development can proceed with building view models and unit tests and once design submits the result, it's a simple question of matching names and properties.
As I mentioned earlier in this post, this is just a taste of convention. For a more fully developed solution, be sure to refer to Rob's presentation.
Here is the application for you to play with, in the order of: code behind, controller, MVVM, and convention. The source code can be downloaded here.