Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Design-Time setup of a ViewModel

I'm using Visual Studio 2013's designer to create my User Control in WPF, and I'm using a MVVM approach.

I'm trying to find the best way to have "Design-Time" setup of my viewmodel so that I immediatly see the effect in the designer of changing a value of a property for instance. I've used different designs and techniques to support this, but nothing is exactly what I want. I'm wondering if someone has better ideas...

Situation (simplified): So I have a "Device" which I want a UserControl to show states and operations. From top to bottom:

  • I have a IDeviceModel which has a field bool IsConnected {get;} (and proper notification of state changes)
  • I have a FakeDeviceModel which implements IDeviceModel, and thus enables me to not rely on a real device for design-time and testing
  • A DeviceViewModel, which contains a IDeviceModel, and encapsulate the model's properties. (yes it has proper INotifyPropertyChanged notifications in it)
  • My UserControl which will have a DataContext of type DeviceViewModel, and would have a custom-styled CheckBox which is IsChecked={Binding IsConnected, Mode=OneWay
  • My Goal: I want to preview on design time how does the Model's IsConnected state affect my UserControl (So it could affect other things than just IsChecked)

Framework:

  • I use the idea of the MVVM Light ViewModelLocator, returning non-static fields (so new instances of ViewModels). At runtime, the real datacontext will be given by the one instanciating this UserControl

d:DataContext="{Binding DeviceViewModelDesignTime, Source={StaticResource ViewModelLocator}}"

 public class ViewModelLocator
 {
    private static MainWindowViewModel _mainWindowViewModel;
    public MainWindowViewModel MainWindowViewModelMainInstance
    {
        get
        {
            if (_mainWindowViewModel == null)
            {
                _mainWindowViewModel = new MainWindowViewModel();
            }
            return _mainWindowViewModel;
        }
    }

    public DeviceViewModel DeviceViewModelDesignTime
    {
        get
        {
            //Custom initialization of the dependencies here
            //Could be to create a FakeDeviceModel and assign to constructor
            var deviceViewModel = new DeviceViewModel();

            //Custom setup of the ViewModel possible here 
            //Could be: deviceViewModel.Model = new FakeDeviceModel();

            return deviceViewModel;
        }
    }

Solutions I tried:

Compile-Time solution

Simply code the setup of the ViewModel in the ViewModelLocator.

var deviceViewModel = new DeviceViewModel(fakeDeviceModel);
var fakeDeviceModel = new FakeDeviceModel();
fakeDeviceModel.IsConnected = true;
deviceViewModel.AddDevice(fakeDeviceModel);

Pros: Simple

Cons: That's longer iterations of always going to change the value in code, recompile, go back to designer view, wait for result

Instance in resources and kept static in ViewModelLocator

So I create an instance in XAML and I try to push it in the current ViewModel used by the designer. Not the cleanest way, but worked for a while in simple situation (yes there's some wierdness with the collection, but was with the idea that I could have multiple devices and a current one)

XAML:

<UserControl x:Class="Views.StepExecuteView"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
         mc:Ignorable="d"
         d:DataContext="{Binding DeviceViewModelDesignTime, Source={StaticResource ViewModelLocator}}">
<UserControl.Resources>
    <viewModels:DesignTimeDeviceManager x:Key="DesignTimeDeviceManager">
        <viewModels:DesignTimeDeviceManager.DesignTimeDevices>
            <device:FakeDeviceModel IsConnected="True"
                                    IsBusy="False"
                                    IsTrayOpen="True"
                                    NumberOfChipSlots="4"
                                    />
        </viewModels:DesignTimeDeviceManager.DesignTimeDevices>

 [... CheckBox binding to datacontext and so on...]

And ViewModelLocator.cs:

 public class ViewModelLocator
 {
    private static MainWindowViewModel _mainWindowViewModel;
    public MainWindowViewModel MainWindowViewModelMainInstance
    {
        get
        {
            if (_mainWindowViewModel == null)
            {
                _mainWindowViewModel = new MainWindowViewModel();
            }
            return _mainWindowViewModel;
        }
    }

    public static FakeDeviceModel DeviceModelToAddInDesignTime;
    public DeviceViewModel DeviceViewModelDesignTime
    {
        get
        {
            var deviceViewModel = new DeviceViewModel();
            if (DeviceModelToAddInDesignTime != null)
                deviceViewModel.AddDevice(DeviceModelToAddInDesignTime );

            return deviceViewModel;
        }
    }
}

public class DesignTimeDeviceManager
{
    private ObservableCollection<FakeDeviceModel> _DesignTimeDevices;
    public ObservableCollection<FakeDeviceModel> DesignTimeDevices
    {
        get { return _DesignTimeDevices; }
        set
        {
            if (_DesignTimeDevices != value)
            {
                _DesignTimeDevices = value;
                ViewModelLocator.DeviceModelToAddInDesignTime = value.FirstOrDefault();
            }
        }
    }
}

Pros:

  • Worked super great on one project. the instance that I had in XAML, I could modify the booleans and I would get -immediate- feedback on how it affects my UserControl. So in the simple situation, the CheckBox's "Checked" state would change and I could modify my styling in real-time, without needing to recompile

Cons:

It stopped working in another project, and this by itself I couldn't find the reason why. But after recompiling and changing stuff, the designer would give me exceptions looking like "Cannot cast "FakeDeviceModel" to "FakeDeviceModel""!! My guess is that the Designer internally compiles and uses caches for those types (C:\Users\firstname.lastname\AppData\Local\Microsoft\VisualStudio\12.0\Designer\ShadowCache). And that in my solution, depending on the ordering of things, I was creating a "FakeDeviceModel" which was assigned to a static instances, and "later on", the next time the ViewModelLocator would be asked for a ViewModel, it would use that instance. However, if in the meantime he "recompiles" or uses a different cache, then it's not "exactly" the same type. So I had to kill the designer (XDescProc) and recompile for it to work, and then fail again a few minutes after. If someone can correct me on this it would be great.

Multi-Binding for d:DataContext and custom converter

The previous solution's problem was pointing me to the fact that the ViewModel and the FakeDeviceModel were created at different moment in time (giving the type/cast problem) and to solve it, I would need to create them at the same time

XAML:

<UserControl x:Class="MeltingControl.Views.DeviceTabView"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
         mc:Ignorable="d">
<d:UserControl.DataContext>
    <MultiBinding Converter="{StaticResource DeviceDataContextConverter}">
        <Binding Path="DeviceViewModelDesignTime" Source="{StaticResource ViewModelLocator}" />
        <Binding>
            <Binding.Source>
                <device:FakeDeviceModel IsConnected="False"
                                    IsBusy="False"
                                    IsTrayOpen="False"
                                    SerialNumber="DesignTimeSerie"
                                    />
            </Binding.Source>
        </Binding>
    </MultiBinding>
</d:UserControl.DataContext>

public class DeviceDataContextConverter: IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (values == null || values.Length == 0)
            return null;

        var vm = (DeviceViewModel)values[0];
        if (values.Length >= 2)
        {
            var device = (IDeviceModel)values[1];
            vm.AddDevice(device);
        }

        return vm;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Pros: -Works super nice! When the binding for the DataContext asks for the ViewModel, I take advantage of the Converter to modify that ViewModel and inject my device before returning it

Cons:

We lose intelissense (with ReSharper), since he doesn't know what type is returned by the converter

Any other ideas or modifications I could make to solve this issue?

like image 391
FrankyB Avatar asked Sep 26 '14 11:09

FrankyB


1 Answers

You may create a design time ViewModel that returns IsConnected = true based on your view mode (FakeDeviceViewModel) and then set it as a design-time data context:

d:DataContext="{d:DesignInstance viewModels:FakeDeviceViewModel, 
IsDesignTimeCreatable=True}"

Where viewModels: is the xaml namespace to the actual view model.

like image 73
IUnknown Avatar answered Sep 20 '22 16:09

IUnknown