Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Expression Blend and Sample data for Dictionary in WPF application

Tags:

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 this in XAML? The non-design time support is killing my productivity.

like image 905
Mark Avatar asked Jun 23 '11 06:06

Mark


1 Answers

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:

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


Setting up the problem

I started with 5 classes (2 were generated from the WPF project template, which I recommend using for this):

  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

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:

An image of what your screen should look like.

What was the problem again?

The problem was that nothing was showing in the design view.

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; } 
like image 110
JoshuaTheMiller Avatar answered Dec 01 '22 19:12

JoshuaTheMiller