Подтвердить что ты не робот

Expression Blend и примеры данных для словаря в приложении WPF

У меня есть приложение WPF, в котором я использую Blend для стиля.

Одна из моих моделей просмотров имеет тип:

public Dictionary<DateTime, ObservableCollection<MyViewModel>> TimesAndEvents

Но когда я пытаюсь создать некоторые примеры данных в Expression Blend, он просто не создает XAML для этого свойства.

Можно ли создать такой тип данных в XAML? Поддержка времени без разработки снижает мою производительность.

4b9b3361

Ответ 1

Я больше не пошел по пути создания экземпляра времени разработки моей модели просмотра в своем локаторе, о котором я упоминал как @ChrisW, предложенного выше:

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

Таким образом, у меня могут быть некоторые жестко закодированные значения для заполнения моих списков, comboboxes и т.д. Делает стиль намного проще.

Я использую MVVM Light, поэтому в моем конструкторе ViewModel я использую такой шаблон:

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

Код будет выполняться только во время разработки, и ваш пользовательский интерфейс Xaml привязан к фактическим данным.

Ответ 2

Что касается вашего последнего вопроса:, к сожалению, вы не можете легко создавать словари в WPF. Я считаю, этот ответ объясняет эту часть хорошо. Книга WPF 4.5 Unleashed дает хорошее резюме того, что говорит связанный ответ:

Общепринятое обходное решение для этого ограничения (неспособное создавать экземпляры словарь в версии XAML в WPF) заключается в получении не общего класс из общего, просто для ссылки на XAML...

Но даже тогда создание этого словаря в xaml снова, на мой взгляд, является болезненным процессом. Кроме того, Blend не знает, как создавать образцы данных этого типа.

Что касается неявного вопроса о том, как получить поддержку времени разработки: существует несколько способов получить данные о времени разработки в WPF, но мой предпочтительный метод на данный момент для сложных сценариев - создать пользовательский DataSourceProvider. Чтобы дать кредит там, где это должно быть: я получил идею от в этой статье (которая даже старше этого вопроса).


Решение DataSourceProvider

Создайте класс, который реализует 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" в <d:Window.DataContext> важно, поскольку он сообщает Blend и компилятору, что этот конкретный элемент предназначен для времени разработки, и его следует игнорировать при компиляции файла.

После этого мой дизайн теперь выглядит следующим образом:

Изображение проектного вида Blend с образцами данных в нем.


Настройка проблемы

Я начал с 5 классов (2 были сгенерированы из шаблона проекта WPF, который я рекомендую для этого):

  • MyViewModel.cs
  • MainWindowViewModel.cs
  • MainWindow.xaml
  • 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

Во-первых, удалите StartupUri="MainWindow.xaml" со стороны 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:

Изображение того, как должен выглядеть ваш экран.

В чем была проблема снова?

Проблема заключалась в том, что в представлении дизайна ничего не показывалось.

Изображение, изображающее пустой экран в представлении дизайна Blend.


Мой метод 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;
}

Ответ 3

Так как 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, связанный с дизайнером, вы получите десятки ошибок. Чтобы решить эту проблему, нам нужно расширение разметки для предоставления значения из Data.xaml с помощью метода XamlReader.Load.

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, чтобы пользователь мог указать, какой файл 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 не будут корректно проверять вас. В конце концов, надеюсь, этого достаточно для ответа.