Expression Blend 和 WPF 应用程序中的字典示例数据

24
我有一个WPF应用程序,现在我正在使用Blend进行样式设计。
我的其中一个视图模型的类型是:
public Dictionary<DateTime, ObservableCollection<MyViewModel>> TimesAndEvents

但是当我尝试在Expression Blend中创建一些样本数据时,它简单地不会为此属性创建XAML。

你能否在XAML中创建这样的数据类型?缺乏设计时支持正在损害我的生产力。


1
仍在寻找这个问题的答案。 - Kasper Holdum
@Sentry 这里可能会有很多问题。你是否提供了一个参考模拟视图模型,并带有实际的设计时间数据来消费?如果是这样的话,你是否可以将其指定为数据上下文作为资源,类似于(在父级上)d:DataContext="{StaticResource PathToDesignTimeDataVMStuffInResources",使其在设计时间中仅仅替换数据上下文为虚假的部分? - Chris W.
@ChrisW。我不到一年前开始接触WPF,所以我还没有完全适应所有的可能性。请原谅我在这里缺乏知识,但我不明白你的问题(模拟视图模型?)与XAML表单中的示例数据有什么关系。 - Sentry
@Sentry 不用担心。我的意思是,你有实际的虚假数据可供在运行时提供给 XAML 中的绑定吗?那些虚假数据必须存放在某个地方。 - Chris W.
@ChrisW。我的假数据就是这样:假的。随机字符串,随机数字。Blend可以为我的大多数视图模型创建它们,但是当有一个字典时,Blend不会生成它。即使是Dictionary<string, string>也不行。 - Sentry
Blend的数据生成功能很差。它甚至无法处理像这个这个这样的简单情况,更不用说复杂类型或复杂类型的字典了。 - dotNET
3个回答

1
关于你上一个问题:不幸的是,在WPF中,你不能轻松地实例化字典。我相信this answer解释得很好。书籍WPF 4.5 Unleashed提供了对链接答案陈述的很好的总结:
“这个限制的常见解决方法(不能在WPF的XAML版本中实例化字典)是从泛型类派生出非泛型类,以便可以从XAML中引用它......”
但即使如此,在xaml中实例化该字典也是一个痛苦的过程。此外,Blend不知道如何创建该类型的示例数据。

关于如何获得设计时支持的隐含问题:在WPF中有几种实现设计时数据的方法,但对于复杂情况,我目前首选的方法是创建自定义的DataSourceProvider。值得一提的是:我从this article中得到了这个想法(该文章甚至比这个问题还要早)。


数据源提供程序解决方案

创建一个实现DataSourceProvider的类,并返回数据上下文的示例。将实例化的MainWindowViewModel传递给OnQueryFinished方法是产生魔力的关键(建议阅读相关内容以了解其工作原理)。

internal class SampleMainWindowViewModelDataProvider : DataSourceProvider
{
    private MainWindowViewModel GenerateSampleData()
    {
        var myViewModel1 = new MyViewModel { EventName = "SampleName1" };
        var myViewModel2 = new MyViewModel { EventName = "SampleName2" };
        var myViewModelCollection1 = new ObservableCollection<MyViewModel> { myViewModel1, myViewModel2 };

        var timeToMyViewModelDictionary = new Dictionary<DateTime, ObservableCollection<MyViewModel>>
        {
            { DateTime.Now, myViewModelCollection1 }
        };

        var viewModel = new MainWindowViewModel()
        {
            TimesAndEvents = timeToMyViewModelDictionary
        };

        return viewModel;
    }

    protected sealed override void BeginQuery()
    {
        OnQueryFinished(GenerateSampleData());
    }
}

现在你需要将你的数据提供程序添加为视图中的示例数据上下文,具体操作如下:

<Window x:Class="SampleDataInBlend.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:SampleDataInBlend"
        mc:Ignorable="d"
        Title="MainWindow" Height="200" Width="300">
    <d:Window.DataContext>
        <local:SampleMainWindowViewModelDataProvider/>
    </d:Window.DataContext>
    <Grid>
        <ListBox ItemsSource="{Binding TimesAndEvents}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding Key}"/>
                        <ListBox ItemsSource="{Binding Value}">
                            <ListBox.ItemTemplate>
                                <DataTemplate DataType="{x:Type local:MyViewModel}">
                                    <TextBlock Text="{Binding EventName}"/>
                                </DataTemplate>
                            </ListBox.ItemTemplate>
                        </ListBox>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>        
    </Grid>
</Window>

注意:<d:Window.DataContext>中的“d”很重要,它告诉Blend和编译器该特定元素仅用于设计时,并且在编译文件时应将其忽略。

这样做后,我的设计视图现在看起来像下面这样:

An image of Blend's design view with sample data in it.


问题设定

我从5个类开始(其中2个是从WPF项目模板生成的,我建议在此使用):

  1. MyViewModel.cs
  2. MainWindowViewModel.cs
  3. MainWindow.xaml
  4. App.xaml

MyViewModel.cs

public class MyViewModel
{
    public string EventName { get; set; }
}

MainWindowViewModel.cs

public class MainWindowViewModel
{
    public IDictionary<DateTime, ObservableCollection<MyViewModel>> TimesAndEvents { get; set; } = new Dictionary<DateTime, ObservableCollection<MyViewModel>>();

    public void Initialize()
    {
        //Does some service call to set the TimesAndEvents property
    }
}

MainWindow.cs

我拿到了生成的MainWindow类并进行了修改。基本上,现在它会要求一个MainWindowViewModel,并将其设置为其DataContext。

public partial class MainWindow : Window
{        
    public MainWindow(MainWindowViewModel viewModel)
    {
        DataContext = viewModel;
        InitializeComponent();
    }
}

MainWindow.xaml

请注意解决方案中缺少设计数据上下文。
<Window x:Class="SampleDataInBlend.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:SampleDataInBlend"
        mc:Ignorable="d"
        Title="MainWindow" Height="200" Width="300">
    <Grid>
        <ListBox ItemsSource="{Binding TimesAndEvents}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding Key}"/>
                        <ListBox ItemsSource="{Binding Value}">
                            <ListBox.ItemTemplate>
                                <DataTemplate DataType="{x:Type local:MyViewModel}">
                                    <TextBlock Text="{Binding EventName}"/>
                                </DataTemplate>
                            </ListBox.ItemTemplate>
                        </ListBox>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>        
    </Grid>
</Window>

App.cs

首先,从xaml代码中删除StartupUri="MainWindow.xaml",因为我们将在后台代码中启动MainWindow。

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        var viewModel = new MainWindowViewModel();
        // MainWindowViewModel needs to have its dictionary filled before its
        // bound to as the IDictionary implementation we are using does not do
        // change notification. That is why were are calling Initialize before
        // passing in the ViewModel.
        viewModel.Initialize();
        var view = new MainWindow(viewModel);

        view.Show();
    }        
}

构建和运行

现在,如果一切都正确并且您完善了MainWindowViewModel的Initialize方法(我将在底部包含我的实现),当您构建和运行WPF应用程序时,您应该看到像下面这样的屏幕:

An image of what your screen should look like.

问题是什么?

问题在于设计视图中没有显示任何内容。

An image depicting a blank screen in Blend's design view.


我的Initialize()方法

public void Initialize()
{
    TimesAndEvents = PretendImAServiceThatGetsDataForMainWindowViewModel();
}

private IDictionary<DateTime, ObservableCollection<MyViewModel>> PretendImAServiceThatGetsDataForMainWindowViewModel()
{
    var myViewModel1 = new MyViewModel { EventName = "I'm real" };
    var myViewModel2 = new MyViewModel { EventName = "I'm real" };
    var myViewModelCollection1 = new ObservableCollection<MyViewModel> { myViewModel1, myViewModel2 };

    var timeToMyViewModelDictionary = new Dictionary<DateTime, ObservableCollection<MyViewModel>>
    {
        { DateTime.Now, myViewModelCollection1 }
    };

    return timeToMyViewModelDictionary;
}

0

自从Xaml 2009支持泛型类型以来,就可以编写一个松散的Xaml(无法在WPF项目中编译)来表示一个字典。

Data.xaml

<gnrc:Dictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 xmlns:sys="clr-namespace:System;assembly=mscorlib"
                 xmlns:gnrc="clr-namespace:System.Collections.Generic;assembly=mscorlib"
                 xmlns:om="clr-namespace:System.Collections.ObjectModel;assembly=System"
                 x:TypeArguments="sys:DateTime,om:ObservableCollection(x:String)">
    <om:ObservableCollection x:TypeArguments="x:String">
        <x:Key>
            <sys:DateTime>2017/12/31</sys:DateTime>
        </x:Key>
        <x:String>The last day of the year.</x:String>
        <x:String>Party with friends.</x:String>
    </om:ObservableCollection>
    <om:ObservableCollection x:TypeArguments="x:String">
        <x:Key>
            <sys:DateTime>2018/1/1</sys:DateTime>
        </x:Key>
        <x:String>Happy new year.</x:String>
        <x:String>Too much booze.</x:String>
    </om:ObservableCollection>
    <om:ObservableCollection x:TypeArguments="x:String">
        <x:Key>
            <sys:DateTime>2018/1/10</sys:DateTime>
        </x:Key>
        <x:String>Just another year.</x:String>
        <x:String>Not much difference.</x:String>
    </om:ObservableCollection>
</gnrc:Dictionary>

但它不受 Blend 或 Visual Studio 等设计工具的支持。如果你把它放到与设计器关联的 xaml 中,你会得到数十个错误。为了解决这个问题,我们需要使用 XamlReader.Load 方法通过标记扩展从 Data.xaml 提供值。

InstanceFromLooseXamlExtension.cs

public class InstanceFromLooseXamlExtension : MarkupExtension
{
    public Uri Source { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        if (Source == null)
        {
            throw new ArgumentNullException(nameof(Source));
        }

        Uri source;
        if (Source.IsAbsoluteUri)
        {
            source = Source;
        }
        else
        {
            var iuc = serviceProvider?.GetService(typeof(IUriContext)) as IUriContext;
            if (iuc == null)
            {
                throw new ArgumentException("Bad service contexts.", nameof(serviceProvider));
            }

            source = new Uri(iuc.BaseUri, Source);
        }

        WebResponse response;
        if (source.IsFile)
        {
            response = WebRequest.Create(source.GetLeftPart(UriPartial.Path)).GetResponse();
        }
        else if(string.Compare(source.Scheme, PackUriHelper.UriSchemePack, StringComparison.Ordinal) == 0)
        {
            var iwrc = new PackWebRequestFactory() as IWebRequestCreate;
            response = iwrc.Create(source).GetResponse();
        }
        else
        {
            throw new ArgumentException("Unsupported Source.", nameof(Source));
        }

        object result;
        try
        {
            result = XamlReader.Load(response.GetResponseStream());
        }
        finally
        {
            response.Close();
        }

        return result;
    }
}

这个标记扩展有一个 Uri 类型的 Source 属性,让用户指定要加载哪个 XAML 文件。最后,像这样使用标记扩展。

MainWindow.xaml

<Window x:Class="WpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <ListBox ItemsSource="{local:InstanceFromLooseXaml Source=/Data.xaml}">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <Expander Header="{Binding Key}">
                    <ListBox ItemsSource="{Binding Value}"/>
                </Expander>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</Window>

在这种情况下,我将Data.xaml放置在应用程序文件夹中,所以“Source=/Data.xaml”就可以了。每次设计师重新加载(重建将确保它),松散xaml中的内容都将被应用。结果应该看起来像这样:

松散的XAML可以包含几乎所有东西,例如ResourceDictionary或带有UiElements的内容。但是无论是Blend还是Visual Studio都无法正确地为您检查它。最终,希望这足以作为答案。


0

最近我已经采用了创建视图模型的设计时实例的方式,将其放在我的定位器中,并像@ChrisW上面建议的那样进行引用:

d:DataContext="{Binding Source={StaticResource Locator}, Path=DesignTimeVM}"

这样我就可以有一些硬编码的值来填充我的列表、组合框等,这样整个样式就更容易了。

我使用MVVM Light,在我的ViewModel构造函数中,我使用以下模式:

if(IsInDesignMode)
{
  ListUsers = new List<User>();
.
.
.
}

该代码仅在设计时执行,您的Xaml UI将绑定到实际数据。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接