【问题标题】:Design-Time setup of a ViewModelViewModel 的设计时设置
【发布时间】:2014-09-26 11:17:00
【问题描述】:

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

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

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

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

框架:

  • 我使用 MVVM Light ViewModelLocator 的想法,返回非静态字段(因此是 ViewModel 的新实例)。在运行时,真正的数据上下文将由实例化此 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 中拥有的实例,我可以修改布尔值,我会得到关于它如何影响我的 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 是在不同的时间创建的(给出类型/转换问题)并且要解决它,我需要同时创建它们

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

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

缺点:

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

我还有什么其他想法或修改可以解决这个问题吗?

【问题讨论】:

  • 你在 Blend 中试过吗?使用 Visual Studio 2010,在 Blend 中使用此模式比在 Visual Studio 中更容易,因为设计器更强大。我不确定 Blend 与 VS2013 相比如何。
  • 你在 Blend 中是如何处理这个问题的?
  • VS 和 Blend 现在共享同一个设计器,但无论如何功能并不完全相同。尽管 VS 不再使用苹果酒设计器,但您现在仍然可以在 VS 中做很多您可以/可以做的事情。

标签: wpf mvvm visual-studio-2013 blend


【解决方案1】:

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

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

viewModels: 是实际视图模型的 xaml 命名空间。

【讨论】:

  • d 命名空间可能不会自动添加。在设计时查看 Visual Studio 的文档查看器并关闭控件时添加它,但在 Blend 中工作时也会添加它。因此完整的命名空间是xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
【解决方案2】:

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

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

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

【讨论】:

    【解决方案3】:

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

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

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

    public class ExampleViewModel : ViewModelBase
    {
        public ExampleViewModel()
        {
            if (IsInDesignMode == true)
            {
                LoadDesignTimeData();
            }
        }
    
        private void LoadDesignTimeData()
        {
            // Load design time data here
        }       
    }
    

    IsInDesignMode 属性可以放置在您的视图模型基类中(如果有的话),如下所示:

    DesignerProperties.GetIsInDesignMode(new DependencyObject());
    

    请看我的回答here

    【讨论】:

      【解决方案4】:

      这就是我使用 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 的构造函数中,检查应用是否处于设计模式,并相应地调用静态方法注册服务。

        public ViewModelLocator()
        {
            if (ViewModelBase.IsInDesignModeStatic)
            {
                MyServiceLocator.RegisterDesignTimeServices();
            }
            else MyServiceLocator.RegisterRunTimeServices();
        }
        
      5. 现在,您的视图只需将 datacontext 设置为相应的视图模型接口而不是视图模型对象。为此,不要将 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 容器中注册设计时或运行时视图模型。这样做的好处是视图模型文件中没有代码混合,并且还可以为多个设计时数据设置代码而不会受到太多干扰。如果您希望在应用程序运行时显示设计时数据,那么只需更改一行代码即可。

      【讨论】:

        【解决方案5】:
        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

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

        【讨论】:

          猜你喜欢
          • 2013-01-02
          • 2012-04-10
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-10-11
          • 2022-01-16
          • 1970-01-01
          相关资源
          最近更新 更多