Expression Blend и примеры данных для словаря в приложении WPF
У меня есть приложение WPF, в котором я использую Blend для стиля.
Одна из моих моделей просмотров имеет тип:
public Dictionary<DateTime, ObservableCollection<MyViewModel>> TimesAndEvents
Но когда я пытаюсь создать некоторые примеры данных в Expression Blend, он просто не создает XAML для этого свойства.
Можно ли создать такой тип данных в XAML? Поддержка времени без разработки снижает мою производительность.
Ответы
Ответ 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 не будут корректно проверять вас. В конце концов, надеюсь, этого достаточно для ответа.