XAML 如何通过UserControl的示例化来获得视图模型的设计示例?

f87krz0w  于 11个月前  发布在  其他
关注(0)|答案(1)|浏览(89)

我有一个带有视图模型的UserControl:

public partial class FarmLogPageView : UserControl
{
    public FarmLogPage? ViewModel
    {
        get => (FarmLogPage)DataContext;
        set => DataContext = value;
    }

    public FarmLogPageView()
    {
        InitializeComponent();

        var dir = "D:\\_\\FolderQuantTests\\FarmLogPages\\";
        var files = Directory.EnumerateFiles(dir);
        string json = File.ReadAllText(files.First());
        ViewModel = JsonSerializer.Deserialize<FarmLogPage>(json);
    }
}

字符串
我还确保在xaml端定义了类型:

d:DataContext="{d:DesignInstance Type=diskManagerCore:FarmLogPage,
    IsDesignTimeCreatable=False}"


但是我的构造函数文本是空的(根据构造函数后面的代码,我应该有一些东西)。我确保了视图模型的无参数构造函数。
就好像我的构造函数后面的代码被忽略了。
如何让视图模型的设计示例基于文件中的序列化数据?
PS此测试通过(即,我有正确的类示例内容):

[TestMethod]
public void DeserializeTest()
{
    var dir = "D:\\_\\FolderQuantTests\\FarmLogPages\\";
    var file = Directory.EnumerateFiles(dir).First(o => o.Contains("Drive"));
    Console.WriteLine($"using {file}");

    string json = File.ReadAllText(file);
    var sut = JsonSerializer.Deserialize<FarmLogPage>(json);
    
    Assert.IsNotNull(sut);
    Assert.IsTrue(sut.ContentMap.Count > 0);
    Assert.AreEqual(28, sut.ContentMap.Count);
    //sut.ContentMap.ToCW();
}


完整视图:

<UserControl x:Class="DiskManagerUI.FarmLogPageView"
             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" 
             xmlns:local="clr-namespace:DiskManagerUI"
             xmlns:diskManagerCore="clr-namespace:DiskManagerCore;assembly=DiskManagerCore"
             d:DataContext="{d:DesignInstance Type=diskManagerCore:FarmLogPage, IsDesignTimeCreatable=False}"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="27"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <StackPanel Grid.Row="0" Orientation="Horizontal">
            <StackPanel.Resources>

            </StackPanel.Resources>
            <TextBlock Text="{Binding PageType, FallbackValue=PageType}" Margin="0 0 10 0"/>
            <TextBlock Text="{Binding ErrorRate, FallbackValue=errorrate}" Margin="0 0 10 0"/>
            <TextBlock Text="{Binding Name, FallbackValue=name}" Margin="0 0 10 0"/>
        </StackPanel>

        <ListBox Grid.Row="1" ItemsSource="{Binding ContentMap}" BorderThickness="0" Background="AliceBlue">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Border BorderBrush="Black" BorderThickness="1">
                        <StackPanel>
                            <TextBlock Text="{Binding Key}" />
                            <TextBlock Text="{Binding Value}" />
                        </StackPanel>
                    </Border>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</UserControl>


验证码:

using DiskManagerCore;
using System.IO;
using System.Text.Json;
using System.Windows.Controls;

namespace DiskManagerUI
{
    /// <summary>
    /// Interaction logic for FarmLogPageView.xaml
    /// </summary>
    public partial class FarmLogPageView : UserControl
    {
        public FarmLogPage? ViewModel
        {
            get => (FarmLogPage)DataContext;
            set => DataContext = value;
        }

        public FarmLogPageView()
        {
            InitializeComponent();

            var dir = "D:\\_\\FolderQuantTests\\FarmLogPages\\";
            var file = Directory.EnumerateFiles(dir).First(o => o.Contains("Drive"));
            string json = File.ReadAllText(file);
            ViewModel = JsonSerializer.Deserialize<FarmLogPage>(json);
        }
    }
}


视图模型:

[Serializable]
public class FarmLogPage
{
    protected string[] Content { get; set; }

    public string Name { get; set; }
    public FarmPageType PageType { get; set; }
    public bool HasContentMap { get; set; }
    public Dictionary<string, string> ContentMap { get; set; } = new();
    
    /// <summary>
    /// For tests or design
    /// </summary>
    public FarmLogPage()
    {
    }
}

cclgggtu

cclgggtu1#

你不应该从构造函数中对文件进行示例化或者遍历文件系统。这会使类型的示例化成为一个阻塞操作。这是一种代码味道。
此外,将代码移到构造函数之外还允许您使用异步JsonSerializer API(JsonSerializer.DeserializeAsync),这将进一步提高性能。然后最好调用JsonSerializer.Deserialize的重载,允许传入Stream(或文件路径)。让JsonSerializer处理文件以使其能够提高性能。File.ReadAllText不是最佳选择。通常,我更喜欢使用StreamReader来读取文件,因为它允许异步文件访问(例如StreamReader.ReadToEndAsync)。

public partial class FarmLogPageView : UserControl
{
    public FarmLogPage? ViewModel
    {
        get => (FarmLogPage)DataContext;
        set => DataContext = value;
    }

    public FarmLogPageView()
    {
        InitializeComponent();
    }

    // You can also use the Loaded event if required.
    // Loaded event is raised after Initialized.
    protected override async void OnInitialized(EventArgs e) 
    {
      base.OnInitialized(e);

      var dir = "D:\\_\\FolderQuantTests\\FarmLogPages\\";
      var files = Directory.EnumerateFiles(dir);
      string jsonFile = files.First();
      await using jsonFileStream = File.OpenRead(jsonFile);
      this.ViewModel = await JsonSerializer.DeserializeAsync<FarmLogPage>(jsonFileStream);
    }
}

字符串
最后,要解决这个问题,你必须正确配置设计时数据。你必须将IsDesignTimeCreatable设置为true。否则WPF将创建一个假类型,而不会示例化原始类型(因此永远不会调用构造函数)。
此假类型只是为了向XAML设计器提供类型的大纲,以便Intellisense可以提出代码建议。

d:DataContext="{d:DesignInstance diskManagerCore:FarmLogPage, IsDesignTimeCreatable=True}"


在再次检查了你的代码之后,我意识到我忽略了导致你的问题的设计缺陷。我假设了一个不同的(更干净的)设计,然后你实际上有。
设计器视图中显示的控件的构造函数不是由设计调用的。在当前方案中,XAML引擎通过调用FarmLogPage的默认构造函数来创建FarmLogPageView的示例。但由于在创建设计时数据时绕过了FarmLogPageView的构造函数,就会得到一个空示例(仅使用默认值初始化)在这一点上,您可以使用来自默认构造函数的设计时示例数据初始化FarmLogPage。我想用测试或设计器代码污染我的产品类。
您可以通过改进类设计来解决问题。
因为你将属性命名为ViewModel,所以我假设你正在实现MVVM模式。该模式规定视图不能引用模型或包含模型责任。
阅读和写数据 * 是 * 模范业务。
遵循设计MVVM规则,您必须将文件处理移到Model。即使您没有实现MVVM,也应该遵循建议的设计。关键是对FarmLogPageView隐藏FarmLogPage的内部。这允许FarmLogPageView仅通过调用构造函数来创建示例。这将改善您的整体类设计,因为它清理了责任。FarmLogPageView必须不知道数据的确切来源。否则,它可以自己存储这些数据。我们必须将检索数据的责任转移到存储/处理数据的对象。
作为新类设计的一个副作用,我们可以创建一个简单的视图模型示例并推迟初始化,这允许使用不允许从构造函数执行的异步代码。
简单地修改你的类设计如下:不要让控件通过初始化来初始化数据上下文值。让控件告诉数据上下文类来初始化自己,例如,但是这些细节应该被隐藏。数据上下文类将调用Model类来获取数据(来源未知-只有Model类知道数据源(JSON文件)以及如何对数据进行格式化)。
因为在你的例子中,你需要将一个完整的对象进行封装,所以你必须使用composition并将这些数据作为FarmLogPage类型的属性公开。换句话说,你必须将FarmLogPage的wrap转换为一个新的类型(例如MainViewModel):

FarmLogPage.cs

public class FarmLogPage
{
    protected string[] Content { get; set; }

    public string Name { get; set; }
    public FarmPageType PageType { get; set; }
    public bool HasContentMap { get; set; }
    public Dictionary<string, string> ContentMap { get; set; } = new();
    
    /// <summary>
    /// For tests or design
    /// </summary>
    public FarmLogPage()
    {
    }
}

MainViewModel.cs

class MainViewModel : INotifyPropertyChanged
{
  public FarmLogPage FarmLogPage { get; private set; }
  public bool IsInitialized { get; private set; }
  private DataProvider DataProvider { get; }

  public MainViewModel()
  {
    this.DataProvider = new DataProvider();
  }

  public async Task InitializeAsync()
  {
    if (this.IsInitialized)
    {
      return;
    }

    this.FarmLogPage = await this.DataProvider.GetFarmLogPageAsync();
    this.IsInitilaized = true;
  }
}

DataProvider.cs

模型类。

class DataProvider
{
  public async Task<<FarmLogPage> GetFarmLogPageAsync()
  {
    var dir = "D:\\_\\FolderQuantTests\\FarmLogPages\\";
    var files = Directory.EnumerateFiles(dir);
    string jsonFile = files.First();
    await using jsonFileStream = File.OpenRead(jsonFile);
    var dataModel = await JsonSerializer.DeserializeAsync<FarmLogPage>(jsonFileStream);

    return dataModel;
  }
}

FarmLogPageView.cs

partial class FarmLogPageView
{
  private MainViewModel MainViewModel { get; }

  public FarmLogPageView()
  {
    InitializeComponent();

    this.MainViewModel = new MainViewModel();
  }

  // You can also use the Loaded event if required.
  // Loaded event is raised after Initialized.
  protected override async void OnInitialized(EventArgs e) 
  {
    base.OnInitialized(e);
  
    await this.MainViewModel.InitializeAsync();
    this.DataContext = this.MainViewModel.FarmLogPage;
  }
}


在修复设计之后。您必须创建一个专用的数据上下文设计数据类型。这是必要的,因为您的数据需要在运行时创建。它还没有完成创建FarmLogPage示例。示例必须通过示例化创建。
当你定义

d:DataContext="{d:DesignInstance diskManagerCore:FarmLogPage, IsDesignTimeCreatable=True}"


则XAML引擎不知道必须通过示例化创建示例。它只是调用默认构造函数(导致一个空示例)。
即使FarmLogPageView构造函数已被调用,XAML设计器也会忽略由非线性化创建的FarmLogPage示例。此示例仅通过 * 运行时数据上下文 * 可用。但您已指示设计器使用 * 设计时数据上下文 *:两个不同的示例,两个不同的数据上下文。一个已正确初始化,另一个为空。
现在,为了能够指定一个设计时数据对象,我们必须设计一个数据类型FarmLogPageDesignData,默认情况下使用设计时示例数据初始化:

FarmLogPageDesignData.cs

class FarmLogPageDesignData : FarmLogPage
{
  // Initialize with design data
  public FarmLogPageDesignData()
  {
    this.Name = ...;
    this.PageType = ...;
    this.HasContentMap = ...;
    puthis.ContentMap = ...;
  }
}


然后更新DesignInstance,以便XAML引擎可以创建FarmLogPageDesignData的设计时示例:

d:DataContext="{d:DesignInstance diskManagerCore:FarmLogPageDesignData, IsDesignTimeCreatable=True}"


如果你不想让设计时数据污染你的生产类型,你可以避免FarmLogPageDesignData类型,而是将设计时示例数据的初始化添加到默认构造函数中(我不推荐这样做)。

另一种方法是不初始化整个示例,而是从JSON中读取初始数据并使用它初始化FarmLogPage
由于示例化涉及文件操作的特殊情况,我建议实现专用的设计时数据类(FarmLogPageDesignData)。

相关问题