Thursday, March 24, 2011

Clean Design-Time Friendly ViewModels: A Walkthrough

This is a quick walkthrough to demonstrate how to make what I call "clean" design-time view models. These are view models that provide design-time data for your application, but don't embed that data in the final product. The release DLL and XAP files will only contain run-time support.

First, fire up Visual Studio and create a new Silverlight application. Just use the default template.

Keep all of the defaults on the dialog that prompts for hosting the application in a new web site (leave this checked) and use Silverlight 4. There is no need to check the "RIA Services" box for this example. When you confirm the dialog, the default project is created for you.

Next, add a folder called "ViewModels."



In the ViewModels folder, add a new class named MainViewModel. Make it a partial class. Create a string property called "Name", an observable collection of strings called "Widget," and implement INotifyPropertyChanged. Here is the code for the view model:

public partial class MainViewModel : INotifyPropertyChanged 
{
    public MainViewModel()
    {
        Widgets = new ObservableCollection<string>();
    }

    private string _name;
    public string Name
    {
        get { return _name; }
        set
        {
            _name = value;
            RaisePropertyChanged("Name");
        }
    }

    public ObservableCollection<string> Widgets { get; private set; }

    protected void RaisePropertyChanged(string propertyName)
    {
        var handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

As you can see, it is a fully functional view model, and no design-time data has been included yet. Next add a special compiler symbol to allow switching from design mode to release mode. Right click on the Silverlight project and go to the properties tab. Click on the "Build" sub-tab, and add the "DESIGN" compiler symbol:

Next, add a class that extends the view model partial class. Name it MainViewModel.sampledata.cs. Place this code in the file:

public partial class MainViewModel
{
#if DESIGN
    private void _WireDesignerData()
    {
        if (!DesignerProperties.IsInDesignTool) return;

        Name = "This is a design-time name.";
        var widgets = new[] { "Widget One", "A Second Widget", "The Third and final Widget" };
        foreach (var widget in widgets)
        {
            Widgets.Add(widget);
        }
    }
#endif
}

Notice the use of the compilation symbol that was added earlier. When you save the file, your solution should look like this:

Wire the call to the new method from the main view model class, wrapping it in the conditional tag - you may have to compile first to get Intellisense for the design-time method:

public MainViewModel()
        {
            Widgets = new ObservableCollection<string>();
#if DESIGN
            _WireDesignerData();
#endif
        }

Build the solution so the view model is available for wiring into the designer surface. Finally, go into the MainPage.xaml and add a grid and a list box bound to the view model. Be sure to add the namespace for your view model:

xmlns:ViewModels="clr-namespace:DesignFriendlyExample.ViewModels" mc:Ignorable="d"

Then wire the remaining XAML:

<Grid x:Name="LayoutRoot" Background="White">
    <Grid.DataContext>
        <ViewModels:MainViewModel/>
    </Grid.DataContext>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <TextBlock Grid.Row="0" Text="{Binding Name}"/>
    <ListBox Grid.Row="1" ItemsSource="{Binding Widgets}"/>
</Grid>

You should immediately see the design-time data appear in the designer:

You can now design with designer data. If you compile and run the application, you'll see the page is blank because the design-time data only is populated in the designer. The only problem is that the XAP and DLL right now are "dirty." If you look at the DLL generated and open it using ILDASM this is what you'll find:

The design-time data has made the DLL larger by adding extra methods and text literals to load into the view models, and adds code that is not used in the run time. When you are ready to release your Silverlight application, simply go back to the properties for the Silverlight project and remove the "DESIGN" compilation symbol. Rebuild the project - it will build and run fine. However, if you take a look at the generated DLL now, the design-time data is no longer there and the DLL is smaller because the data has not been included - as you can see, the method to wire the data doesn't even exist on the view model:

Obviously this example worked with a very specific way of adding the view model to the view. Different frameworks have different ways of binding views and view models. This walkthrough should give you an idea of how to keep te design-time data separate regardless of the method you use and produce cleaner applications when design-time data is no longer needed.

Addendum

Thanks for the tip on this (see comments) ... here's an even cleaner way to build the sample data:

public partial class MainViewModel
{
    [Conditional("DESIGN")]
    private void _WireDesignerData()
    {
        if (!DesignerProperties.IsInDesignTool) return;

        Name = "This is a design-time name.";
        var widgets = new[] { "Widget One", "A Second Widget", "The Third and final Widget" };
        foreach (var widget in widgets)
        {
            Widgets.Add(widget);
        }
    }
}    

And because the directive is conditional, you don't have to condition calls to the method. The main view model constructor now looks like this:

public MainViewModel()
{
    Widgets = new ObservableCollection();
    _WireDesignerData();
}

And the compiler will automatically ignore the method call if the DESIGN symbol is not present. How cool and easy is that?

PS - Bonus round. If you name your sample data as [viewmodel].designer.cs, you automatically get association between the sample class and the view model. The result will look like this in the IDE:



Jeremy Likness

12 comments:

  1. I really like this! This is soooooo much better then what I've been doing.

    One small suggestion to make it just a bit cooler... instead of using the #if approach, check out the [Conditional("DESIGN")] attribute. The advantage is that the compiler will remove the call to _WireDesignerData() in the constructor for you automagically (no need for a #if there either).

    Thanks again.

    ReplyDelete
  2. Wow! I didn't even know the DESIGN condition existed! I'm testing this right away!

    Thanks, Jeremy. I always learn something new from your posts!

    ReplyDelete
  3. How would this work with an ItemsControl thats using a DataTemplate?

    ReplyDelete
  4. No good solution for that right now, short of building in a design-time data template selector that knows where to find design-time data. Good point though!

    ReplyDelete
  5. This is a nice approach. I have handled design time data via dependency injection in the past, which I think has some advantages in larger projects. Some people call it the repository pattern, where basically your viewmodel takes a dependency on ISomeDataSource, and you inject different implementation of ISomeDataSource into the viewmodel in various circumstances (runtime, design time, test). I have used DesignerProperties.IsInDesignTool in that context to detect a designer is being used.

    However I can see that the 'DESIGN' conditional would work nicely in that architecture, as whatever creates the implementation of ISomeDataSource can be excluded from the runtime build as per your example.

    ReplyDelete
  6. Fernando Urkijo CerecedaMay 4, 2011 at 9:49 AM

    Nice post, bur for designt time tada I use the expression blend to create data sample from a CLR class, i think it's fastest and cleaner, what are the differences?

    ReplyDelete
  7. It depends on the complexity of the project and availability of the class. We do a lot of projects that require more complex, custom data than the sample data generates (it doesn't do well, for example, with more complex nested classes) and we also typically have a separate design team working in parallel, so defining the contract up front and providing them with data helps them design more quickly rather than trying to inspect the class and determine what it is/does/means. Obviously if the developer is also the designer it's a different paradigm.

    ReplyDelete
  8. In Silverlight, you can replace the DESIGN compiler directive with this:
    public bool IsDesignTime()
    {
    return DesignerProperties.GetIsInDesignMode(Application.Current.RootVisual);
    }

    ReplyDelete
  9. Jax, not true - you missed the point. I am well aware of the use of the design mode method to determine if the code is being executed at design-time or run time. The purpose of the compiler directive is not to condition the call to the method, but to condition the compiling of the method. If you just check the designer flag, the design-time code gets compiled into the target XAP and you have additional code you need need. When using the compiler directive, if the directive isn't there, the method doesn't even get compiled or included in the output, allowing for a leaner production XAP without the design-time logic.

    ReplyDelete
  10. ah of course - thanks for clearing that up :)

    ReplyDelete
  11. PS: Instead of removing the DESIGN compiler directive as suggested, why not just change the build configuration to something else (Like "RELEASE") before deployment. Each build configuration maintains it's own set if compiler directives.

    ReplyDelete
  12. Thanks for sharing this nice tip. It's worth noting that if you use the [Conditional("DESIGN")]
    strategy the method call will indeed be stripped out but the method is still included in your assembly. The only way to exclude the sample data is to use the #if DESIGN pattern as originally suggested by Jeremy.

    ReplyDelete