ViewModel的设计时设置

时间:2014-09-26 11:17:00

标签: wpf mvvm visual-studio-2013 blend

我正在使用Visual Studio 2013的设计器在WPF中创建用户控件,而我正在使用MVVM方法。

我正在尝试找到设置我的viewmodel的“Design-Time”的最佳方法,以便我立即看到设计师在更改属性值时的效果。我使用了不同的设计和技术来支持这一点,但没有什么是我想要的。我想知道是否有人有更好的想法...

情况(简化): 所以我有一个“设备”,我希望UserControl显示状态和操作。从上到下:

  • 我有一个IDeviceModel,它有一个字段bool IsConnected {get;}(以及状态变化的正确通知)
  • 我有一个实现IDeviceModel的FakeDeviceModel,因此我可以不依赖真实设备进行设计时和测试
  • DeviceViewModel,包含IDeviceModel,并封装模型的属性。 (是的,它有适当的INotifyPropertyChanged通知)
  • 我的UserControl将具有类型为DeviceViewModel的DataContext,并且具有自定义样式的CheckBox IsChecked={Binding IsConnected, Mode=OneWay
  • 我的目标:我想在设计时预览Model的IsConnected状态如何影响我的UserControl(所以它可能会影响除IsChecked之外的其他事情)

框架:

  • 我使用MVVM Light ViewModelLocator的想法,返回非静态字段(因此ViewModels的新实例)。在运行时,真实的datacontext将由实现此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;
        }
    }

我试过的解决方案:

编译时解决方案

只需在ViewModelLocator中编写ViewModel的设置。

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

优点:简单

缺点:总是要更改代码中的值,重新编译,返回设计器视图,等待结果,这是更长的迭代

资源中的实例并在ViewModelLocator中保持静态

所以我在XAML中创建了一个实例,并尝试将其推送到设计者使用的当前ViewModel中。不是最干净的方式,但在简单的情况下工作了一段时间(是的,这个系列有一些奇怪的东西,但我的想法是我可以拥有多个设备和当前的设备)

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...]

和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();
            }
        }
    }
}

优点:

  • 在一个项目上工作得很好。我在XAML中的实例,我可以修改布尔值,我会得到-immediate-反馈它如何影响我的UserControl。所以在简单的情况下,CheckBox的“Checked”状态会改变,我可以实时修改我的样式,而无需重新编译

缺点:

它停止了在另一个项目中工作,而这本身就找不到原因。但是在重新编译和更改内容之后,设计师会给我一些异常,看起来像“无法施放”FakeDeviceModel“到”FakeDeviceModel“”!!我的猜测是Designer内部编译并使用这些类型的缓存(C:\ Users \ firstname.lastname \ AppData \ Local \ Microsoft \ VisualStudio \ 12.0 \ Designer \ ShadowCache)。在我的解决方案中,根据事物的顺序,我创建了一个分配给静态实例的“FakeDeviceModel”,并且“稍后”,下次ViewModelLocator被要求提供ViewModel时,它会使用它实例。但是,如果在此期间他“重新编译”或使用不同的缓存,那么它不是“完全”相同的类型。所以我不得不杀死设计师(XDescProc)并重新编译它才能工作,然后几分钟后再次失败。如果有人可以纠正我,那就太好了。

d:DataContext和自定义转换器的多重绑定

之前的解决方案的问题是指向ViewModel和FakeDeviceModel是在不同的时刻创建(给出类型/强制转换问题)并解决它的事实,我需要同时创建它们< / p>

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();
    }
}

优点: -Works超级棒!当DataContext的绑定请求ViewModel时,我利用Converter修改ViewModel并在返回之前注入我的设备

缺点:

我们失去了intelissense(使用ReSharper),因为他不知道转换器返回的是什么类型

我可以为解决这个问题做出任何其他想法或修改吗?

4 个答案:

答案 0 :(得分:4)

您可以创建一个设计时ViewModel,它返回IsConnected = true(FakeDeviceViewModel)并将其设置为设计时数据上下文:

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

答案 1 :(得分:1)

我想提出一个替代解决方案。

您可以将相同的视图模型用于设计时数据和正常运行时,并检查您的(单个)视图模型是否设计器处于活动状态,然后在那里加载设计时数据。

在您的视图模型中,您可以执行以下操作:

public class ExampleViewModel : ViewModelBase
{
    public ExampleViewModel()
    {
        if (IsInDesignMode == true)
        {
            LoadDesignTimeData();
        }
    }

    private void LoadDesignTimeData()
    {
        // Load design time data here
    }       
}

IsInDesignMode属性可以放在视图模型基类中 - 如果有的话 - 看起来像这样:

DesignerProperties.GetIsInDesignMode(new DependencyObject());

请查看我的回答here

答案 2 :(得分:1)

这是我使用MVVMLight进行的项目之一。

  1. 为每个需要单独设计时间,运行时属性和行为的视图模型创建界面。
  2. 为每个视图创建单独的视图模型-一个用于运行时,另一个用于设计时。从上面定义的同一接口派生两个视图模型。
  3. 创建一个具有两个静态方法的静态类-一个用于在IOC容器中注册运行时服务,另一个用于在IOC容器中注册设计时服务。我使用相同的SimpleIOC.Default容器。在绑定到其接口的两种方法中注册适当的视图模型。

    public static class MyServiceLocator()
    {
        public static void RegisterRunTimeServices()
        {
            ServiceLocator.SetLocatorProvider(() => SimpleIOC.Default);
            SimpleIoc.Default.Register<MainViewModel>();
            SimpleIoc.Default.Register<IAboutViewModel, AboutViewModel>();
        }
    
        public static void RegisterDesignTimeServices()
        {
            ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
            SimpleIoc.Default.Register<MainViewModel>();
            SimpleIoc.Default.Register<IAboutViewModel, DesignTimeAboutViewModel>();
        }
    
  4. 在ViewModelLocator的构造函数中,检查应用程序是否处于DesignMode并相应地调用静态方法以注册服务。

    public ViewModelLocator()
    {
        if (ViewModelBase.IsInDesignModeStatic)
        {
            MyServiceLocator.RegisterDesignTimeServices();
        }
        else MyServiceLocator.RegisterRunTimeServices();
    }
    
  5. 现在,您的View只需将datacontext设置为对应的viewmodel接口,而不是viewmodel对象。为此,不要公开ViewModel对象到ViewModelLocator的每个视图中,而是公开viewmodel接口。

    在ViewModelLocator.cs

    public IAboutViewModel About
    {
        get
        {
            return ServiceLocator.Current.GetInstance<IAboutViewModel>();
        }
    }
    

    在AboutView.xaml中

    DataContext="{Binding Source={StaticResource Locator}, Path=About}"
    
  6. 在代码中需要的地方,请将接口强制转换为ViewModelBase类型,以将其转换为ViewModel对象并使用。

    在MainViewModel.cs

    public class MainViewModel : ViewModelBase
    {
        private readonly IAboutViewModel _aboutViewModel;
    
        public MainViewModel()
        {
            _aboutViewModel = ServiceLocator.Current.GetInstance<IAboutViewModel>();
            CurrentViewModel = (ViewModelBase) _aboutViewModel;
        }
    }
    

因此,基本上,我使用DI向每个视图注入适当的视图模型,具体取决于代码是在运行时还是在设计时。 ViewModelLocator只是在SimpleIOC容器中注册设计时或运行时视图模型。这样做的好处是,视图模型文件中不会混合任何代码,而且还可以为多个设计时数据设置代码而不会产生太多干扰。如果您希望在应用程序运行时显示设计时数据,那么只需一行代码更改即可。

答案 3 :(得分:0)

我已经详细记录了我是如何设法在Visual Studio中查看实时设计时数据的完美设置。

在此页面上查看Hint 9 - Design Time DataContext

ReSharper WPF error: "Cannot resolve symbol "MyVariable" due to unknown DataContext"