Expression Blend and Sample data for Dictionary in WPF application
I have a WPF app which I am using Blend to style.
One of my view models is of the type:
public Dictionary<DateTime, ObservableCollection<MyViewModel>> TimesAndEvents
But when I try to create some sample data in Expression Blend it simply doesnt create the XAML for this property.
Can you create a data type like thi开发者_高级运维s in XAML? The non-design time support is killing my productivity.
Regarding your last question: unfortunately, you cannot easily instantiate dictionaries in WPF. I believe this answer explains that part well. The book, WPF 4.5 Unleashed provides a good summary of what the linked answer states:
A common workaround for this limitation (not being able to instantiate a dictionary in WPF's version of XAML) is to derive a non-generic class from a generic one simply so it can be referenced from XAML...
But even then, instantiating that dictionary in xaml is again, in my opinion, a painful process. Additionally, Blend does not know how to create sample data of that type.
Regarding the implicit question of how to get design time support: there are a few ways to achieve design time data in WPF, but my preferred method at this point in time for complex scenarios is to create a custom DataSourceProvider. To give credit where it is due: I got the idea from this article (which is even older than this question).
The DataSourceProvider Solution
Create a class that implements DataSourceProvider and returns a sample of your data context. Passing the instantiated MainWindowViewModel to the OnQueryFinished method is what makes the magic happen (I suggest reading about it to understand how it works).
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());
}
}
All that you have to do now is add your data provider as a sample data context in your view:
<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>
Note: the 'd' in <d:Window.DataContext>
is important as it tells Blend and the compiler that that specific element is for design time and it should be ignored when the file is compiled.
After doing that, my design view now looks like the following:
Setting up the problem
I started with 5 classes (2 were generated from the WPF project template, which I recommend using for this):
- 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
I took the generated MainWindow class and changed it. Basically, now it asks for a MainWindowViewModel and sets it as its DataContext.
public partial class MainWindow : Window
{
public MainWindow(MainWindowViewModel viewModel)
{
DataContext = viewModel;
InitializeComponent();
}
}
MainWindow.xaml
Please note the lack of the design data context from the Solution.
<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
First off, remove StartupUri="MainWindow.xaml"
from the xaml side as we'll be launching MainWindow from the code behind.
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();
}
}
Build and run
Now, if everything was done correctly and you fleshed out MainWindowViewModel's Initialize method (I will include my implementation at the bottom), you should see a screen like the one below when you build and run your WPF app:
What was the problem again?
The problem was that nothing was showing in the 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;
}
Any more I've gone the route of creating a Design Time Instance of my Viewmodel in my Locator that I reference as @ChrisW suggested above:
d:DataContext="{Binding Source={StaticResource Locator}, Path=DesignTimeVM}"
So I can have some hard-coded values to populate my lists, comboboxes, etc. Makes styling everything that much easier.
I use MVVM Light and so in my ViewModel's constructor I use a pattern like this:
if(IsInDesignMode)
{
ListUsers = new List<User>();
.
.
.
}
The code will only execute at Design Time, and you will have your Xaml UI bound to actual data.
Since Xaml 2009 support generic types, is possible write a loose xaml(can not be compiled in wpf project) like this to represent a dictionary.
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>
But it is not support by designers like Blend or Visual Studio. If you put it into a xaml that associated with a designer, you will get dozens of errors. To solve this, we need a markup extension to provide value from Data.xaml by using XamlReader.Load method.
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;
}
}
This markup extension has a Uri type Source property to let user specify which xaml file to load. Then finally, use the markup extension like this.
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>
In this case, I place Data.xaml in application folder, so 'Source=/Data.xaml' will be OK. Every time the designer reloaded(a rebuild will ensure it), the contents in loose xaml will be applied. The result should look like
The loose xaml can contain almost everything, like a ResourceDictionary or something with UiElements. But both Blend or Visual Studio will not check it correctly for you. In the end, hope this is enough for an answer.
精彩评论