Смешивание и выборка данных для словаря в приложении WPF - PullRequest
22 голосов
/ 23 июня 2011

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

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

public Dictionary<DateTime, ObservableCollection<MyViewModel>> TimesAndEvents

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

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

Ответы [ 3 ]

0 голосов
/ 30 октября 2017

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

Обычный обходной путь для этого ограничения (неспособность создать экземпляр словарь в WPF версии XAML) для получения неуниверсального класс из универсального просто так, чтобы на него можно было ссылаться из 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 и компилятору, что этот конкретный элемент предназначен для времени разработки, и его следует игнорировать при компиляции файла.

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

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

Прежде всего, удалите 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();
    }        
}

Сборка и запуск

Теперь, если все было сделано правильно и вы реализовали метод Initialize MainWindowViewModel (я включу мою реализацию внизу), вы должны увидеть экран, подобный показанному ниже, когда вы собираете и запускаете свой WPF приложение:

An image of what your screen should look like.

В чем опять проблема?

Проблема заключалась в том, что в режиме конструктора ничего не отображалось.

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


My Initialize () method

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 голосов
/ 22 января 2018

Поскольку 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;
    }
}

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

0 голосов
/ 12 мая 2017

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

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

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

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

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

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

...