wpf ViewModel 的设计时设置

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/26058308/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-13 12:34:31  来源:igfitidea点击:

Design-Time setup of a ViewModel

wpfmvvmvisual-studio-2013blend

提问by FrankyB

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

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

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:

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

  • 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)
  • 我有一个 IDeviceModel 有一个字段bool IsConnected {get;}(和状态变化的正确通知)
  • 我有一个实现 IDeviceModel 的 FakeDeviceModel,因此我可以不依赖真实设备进行设计和测试
  • 一个 DeviceViewModel,它包含一个 IDeviceModel,并封装了模型的属性。(是的,它有适当的 INotifyPropertyChanged 通知)
  • 我的 UserControl 将有一个 DeviceViewModel 类型的 DataContext,并且有一个自定义样式的 CheckBox,它是 IsChecked={Binding IsConnected, Mode=OneWay
  • 我的目标:我想在设计时预览模型的 IsConnected 状态如何影响我的 UserControl(因此它可能会影响其他事物,而不仅仅是 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
  • 我使用 MVVM Light ViewModelLocator 的想法,返回非静态字段(因此是 ViewModel 的新实例)。在运行时,真正的数据上下文将由实例化此 UserControl 的人提供

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

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.

只需在 ViewModelLocator 中对 ViewModel 的设置进行编码。

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

在资源中实例化并在 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 中创建了一个实例,并尝试将它推送到设计器使用的当前 ViewModel 中。不是最干净的方式,但在简单的情况下工作了一段时间(是的,该系列有些奇怪,但我的想法是我可以拥有多个设备和一个当前设备)

XAML:

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:

和 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
  • 在一个项目上工作得非常好。我在 XAML 中拥有的实例,我可以修改布尔值,我会立即获得关于它如何影响我的 UserControl 的反馈。所以在简单的情况下,CheckBox 的“Checked”状态会改变,我可以实时修改我的样式,而无需重新编译

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.

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

Multi-Binding for d:DataContext and custom converter

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

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

上一个解决方案的问题是我指出 ViewModel 和 FakeDeviceModel 是在不同时间创建的事实(给出类型/转换问题),为了解决它,我需要同时创建它们

XAML:

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

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

Cons:

缺点:

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

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

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

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

回答by IUnknown

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

您可以创建一个IsConnected = true基于您的视图模式 ( FakeDeviceViewModel)返回的设计时 ViewModel,然后将其设置为设计时数据上下文:

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

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

viewModels:实际视图模型的 xaml 命名空间在哪里。

回答by Contango

I've documented exactly how I managed to get the perfect setup to see live Design Time data in Visual Studio.

我已经准确地记录了我如何设法获得完美设置以在 Visual Studio 中查看实时设计时数据。

Look at Hint 9 - Design Time DataContexton this page:

看看Hint 9 - Design Time DataContext这个页面:

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

ReSharper WPF 错误:“由于未知的 DataContext,无法解析符号“MyVariable””

回答by Martin

I would like to propose an alternative solution.

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

You could use that same view model for design time data and normal runtime, and check in your (single) viewmodel whether the designer is active and then load the design time data there.

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

In your view model you would do something like this:

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

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

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

The IsInDesignModeproperty could be placed in your view model base class - if you have one - and looks like this:

IsInDesignMode属性可以放置在您的视图模型基类中 - 如果您有一个 - 并且如下所示:

DesignerProperties.GetIsInDesignMode(new DependencyObject());

Please take a look at my answer here

请在这里查看我的回答

回答by bedunadain

This is how I am doing it one of my projects using MVVMLight.

这就是我使用 MVVMLight 完成我的项目之一的方式。

  1. Create interface for every viewmodel for which you need separate design time and run time properties and behavior.
  2. Make separate viewmodels for every view - one for run time and another for design time. Derive both viewmodels from the same interface defined above.
  3. Create a static class that has two static methods - one for registering services for run time in the IOC container and another for registering services for design time in the IOC container. I use the same SimpleIOC.Default container. Register appropriate viewmodels in both the methods bound to their interfaces.

    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. In constructor of ViewModelLocator, check if the app is in DesignMode and accordingly call the static method to register services.

    public ViewModelLocator()
    {
        if (ViewModelBase.IsInDesignModeStatic)
        {
            MyServiceLocator.RegisterDesignTimeServices();
        }
        else MyServiceLocator.RegisterRunTimeServices();
    }
    
  5. Now, your Views just have to set datacontext as corresponding viewmodel interface instead of viewmodel object. To do that, instead of exposing viewmodel objects to each view from the ViewModelLocator, expose viewmodel interface.

    In ViewModelLocator.cs

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

    In AboutView.xaml

    DataContext="{Binding Source={StaticResource Locator}, Path=About}"
    
  6. Wherever needed in code, cast the interface to ViewModelBase type to convert it to ViewModel object and use.

    In MainViewModel.cs

    public class MainViewModel : ViewModelBase
    {
        private readonly IAboutViewModel _aboutViewModel;
    
        public MainViewModel()
        {
            _aboutViewModel = ServiceLocator.Current.GetInstance<IAboutViewModel>();
            CurrentViewModel = (ViewModelBase) _aboutViewModel;
        }
    }
    
  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. 现在,您的视图只需将 datacontext 设置为相应的视图模型接口而不是视图模型对象。为此,不要将视图模型对象从 ViewModelLocator 公开给每个视图,而是公开视图模型接口。

    在 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;
        }
    }
    

So, basically I use DI to inject appropriate viewmodels to each view depending on whether the code is in run time or design time. The ViewModelLocator simply registers either design time or run time viewmodels in the SimpleIOC container. The benefit of this is that there is no mixing of code in viewmodel files and one can also setup the code for multiple design time data without much interference. If you want designtime data to show while the application runs, then also its possible with one line change in code.

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

回答by thisisgoke

  1. I would create an instance of Fake View Model in a separate xaml e.g. DeviceViewModelDataSample.xaml (see example below)

  2. Set Build Action to DesignData

  3. Reference the file as such

       <UserControl x:Class="YourNameSpace.YourControl"
                    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
                    mc:Ignorable="d" 
                    d:DataContext="{d:DesignData Source=/DataSample/DeviceViewModelDataSample.xaml}">
    <!-- Skiped details for brevity -->   
    </UserControl>
    
  1. 我会在一个单独的 xaml 中创建一个假视图模型的实例,例如 DeviceViewModelDataSample.xaml(见下面的例子)

  2. 将构建操作设置为DesignData

  3. 像这样引用文件

       <UserControl x:Class="YourNameSpace.YourControl"
                    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
                    mc:Ignorable="d" 
                    d:DataContext="{d:DesignData Source=/DataSample/DeviceViewModelDataSample.xaml}">
    <!-- Skiped details for brevity -->   
    </UserControl>
    


DeviceViewModelDataSample.xaml

DeviceViewModelDataSample.xaml

<vm:DeviceViewModel xmlns:dm="clr-namespace:YourNameSpace.DataModel" 
                xmlns:vm="clr-namespace:YourNameSpace.ViewModel">
   <vm:DeviceViewModel.DeviceManager> <!-- Assuming this is a collection -->
        <dm:DeviceModel DeviceName="Fake Device" IsConnected ="true" /> <!-- This creates an instance at design time -->
   </vm:DeviceViewModel.DeviceManager>    
</vm:DeviceViewModel>