I have a general question about data templates in WPF. Let's say I have an abstract class called "Question," and various subclasses like "MathQuestion," "GeographyQuestion," etc. In some contexts, rendering the questions as a "Question" using the "Question" data template is good enough, but let's say that I have a list of random Question objects of varying subclasses that I want to display in-turn. I want to display them to the user using their specific data templates rather than their generic Question data template, but since I don't know that at design time, is there anyway to tell WPF, "hey, here's a list of Quesitons, but use reflection to figure out their specific types and use THAT data template?"
What I've thought of so far: I thought that in addition to having my question collection, I could create another collection of the specific types using reflection and somehow bind that to "blah," then I'd get the desired affect, but you can only bind to DependencyProperties in WPF, so I'm not sure what I'd bind to. I really don't like this idea, and my gut tells me there's a more elegant way to approach this problem.
I'm not looking for specific code here, just a general strategy to accomplish what I'm trying to do. Also, I'm using MVVM for the most part if that helps.
Thanks
I'm thinking something like this should work right out of the box:
<UserControl.Resources>
<DataTemplate DataType="{x:Type vm:GenericQuestionViewModel}">
<v:GenericQuestion/>
</DataTemplate>
<DataTemplate DataType="{x:Type tvm:GeographyQuestionViewModel}">
<tv:GeographyQuestion/>
</DataTemplate>
<DataTemplate DataType="{x:Type tvm:BiologyQuestionViewModel}">
<tv:BiologyQuestion/>
</DataTemplate>
</UserControl.Resources>
<ContentControl Content="{Binding QuestionViewModel}">
Edit:
Yes, this definitely should work. Here's a more complete example:
Main View Model
public class MainWindowViewModel : ViewModelBase
{
public ObservableCollection<QuestionViewModel> QuestionViewModels { get; set; }
public MainWindowViewModel()
{
QuestionViewModels = new ObservableCollection<QuestionViewModel>
{
new GenericQuestionViewModel(),
new GeographyQuestionViewModel(),
new BiologyQuestionViewModel()
};
}
}
Question View Models
public abstract class QuestionViewModel : ViewModelBase
{
}
public class GenericQuestionViewModel : QuestionViewModel
{
}
public class GeographyQuestionViewModel : QuestionViewModel
{
}
public class BiologyQuestionViewModel : QuestionViewModel
{
}
Question User Controls
<UserControl x:Class="WpfApplication1.GenericQuestion" ...>
<Grid>
<TextBlock Text="Generic Question" />
</Grid>
</UserControl>
<UserControl x:Class="WpfApplication1.GeographyQuestion" ...>
<Grid>
<TextBlock Text="Geography Question" />
</Grid>
</UserControl>
<UserControl x:Class="WpfApplication1.BiologyQuestion" ...>
<Grid>
<TextBlock Text="Biology Question" />
</Grid>
</UserControl>
Main Window
<Window x:Class="WpfApplication1.MainWindow" ...
Title="MainWindow"
Height="900"
Width="525">
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<Window.Resources>
<DataTemplate DataType="{x:Type local:GenericQuestionViewModel}">
<local:GenericQuestion />
</DataTemplate>
<DataTemplate DataType="{x:Type local:GeographyQuestionViewModel}">
<local:GeographyQuestion />
</DataTemplate>
<DataTemplate DataType="{x:Type local:BiologyQuestionViewModel}">
<local:BiologyQuestion />
</DataTemplate>
</Window.Resources>
<ItemsControl ItemsSource="{Binding QuestionViewModels}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl Content="{Binding}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Window>
Update
Kyle Tolle pointed out a nice simplification for setting ItemsControl.ItemTemplate. Here is the resulting code:
<ItemsControl ItemsSource="{Binding QuestionViewModels}"
ItemTemplate="{Binding}" />
Generally, if you need to dynamically change the DataTemplate based on some non-static logic, you'd use a DataTemplateSelector. Another option it to use DataTriggers in your DataTemplate to modify the look appropriately.
Related
When I define a DataTemplate inline, Visual Studio knows about the type I'm binding to, and properties in that type come up in autocomplete (for example in the code below I was able to select DisplayName from the autocomplete list inside the FirstViewModel template).
<DataTemplate DataType="{x:Type viewmodels:FirstViewModel}">
<StackPanel >
<Label Content="{Binding DisplayName}"/>
</StackPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type viewmodels:SecondViewModel}">
<views:SecondView/>
</DataTemplate>
However, when the data template references an external control, as for SecondViewModel in the code above, when I'm in the file for the SecondView usercontrol, since it's just a control, the type isn't bound and the editor doesn't help me with anything.
I've tried wrapping my whole control (inside the UserControl element) in the same DataTemplate tag, but then my whole view just shows "System.Windows.DataTemplate".
<UserControl x:Class="Gui.Views.Tabs.ExamsTabViews.ExamInfoView"
xmlns:vm="clr-namespace:Gui.ViewModels"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<DataTemplate DataType="vm:ExamInfoViewModel">
<DockPanel VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
<!-- contents of the template -->
</DockPanel>
</DataTemplate>
</UserControl>
Is there a way to achieve this kind of binding for the editor?
<DataTemplate DataType="{x:Type viewmodels:SecondViewModel}">
<views:SecondView/>
</DataTemplate>
when this DataTemplate is instantiated, there will be created SecondView and that SecondView will have a SecondViewModel in DataContext. So there is no need any DataTemplate in SecondViewModel control - bind to DataContext instead ({Binding SecondViewModelProperty}). To have design-time support for such binding use d:DataContext="{d:DesignInstance}:
<UserControl d:DataContext="{d:DesignInstance Type=vm:ExamInfoViewModel,
IsDesignTimeCreatable=True}" ...>
In my project, I wanted a UserControl that can display different formats of an image (bitmap, svg), selected from a ListBox. The SelectedItem of the ListBox is bound to the appropriate view model, which in turn changes the DataContext of the UserControl, and what I want to achieve is for it to change the displaying control (an Image for bitmaps, a SharpVectors.SvgViewBox for svg files) through data templates. It does so, but raises data binding errors, as if the templates were still intact whilst the UserControl's DataContext has already been changed.
I should like to a) avoid any data binding errors even if they cause no visible problems b) understand what is happening, so I prepared a MWE, which, to my surprise, displays the same behaviour, so I can present it here.
My minimal UserControl is as follows:
<UserControl x:Class="BindingDataTemplateMWE.VersatileControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:system="clr-namespace:System;assembly=mscorlib"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<DataTemplate DataType="{x:Type system:String}">
<TextBlock Text="{Binding Content.Length, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContentControl}}" />
</DataTemplate>
<DataTemplate DataType="{x:Type system:DateTime}">
<TextBlock Text="{Binding Content.DayOfWeek, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContentControl}}" />
</DataTemplate>
</UserControl.Resources>
<Grid>
<ContentControl
Content="{Binding .}" />
</Grid>
</UserControl>
The MainWindow that references this UserControl has the following XAML:
<Window x:Class="BindingDataTemplateMWE.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:BindingDataTemplateMWE"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ListBox
Grid.Column="0"
SelectedItem="{Binding Selected}"
ItemsSource="{Binding Items}" />
<local:VersatileControl
Grid.Column="1"
DataContext="{Binding Selected}" />
</Grid>
</Window>
with the following code-behind (to make the MWE indeed minimal, I made the window its own DataContext, but originally there is a dedicated view model):
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
namespace BindingDataTemplateMWE
{
public partial class MainWindow : Window, INotifyPropertyChanged
{
private object selected;
public event PropertyChangedEventHandler PropertyChanged;
public List<object> Items { get; }
public object Selected {
get { return selected; }
set {
selected = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Selected)));
}
}
public MainWindow()
{
InitializeComponent();
DataContext = this;
Items = new List<object>() { "a string", DateTime.Now, "another string" };
}
}
}
When I select a different item in the list, the desired effect takes place: the UserControl displays the length if a string is selected and the day of week when a DateTime. Still, I get the following binding error when selecting a DateTime after a string:
Length property not found on object of type DateTime.
and conversely, selecting a string after a DateTime yields
DayOfWeek property not found on object of type String.
It is clear that what I am doing is not meant to be done, but I do not know what the correct paradigm is and what happens in the background. Please advise me. Thank you.
I've seen this problem often when creating complex data templates (several levels of nesting) when views are loaded/unloaded. Honestly, some of such errors I am ignoring completely.
In your case something similar happens because you are manipulating DataContext directly. At the moment the new value is set, the previous value is still used in bindings, which monitor for source change and will try to update the target.
In your scenario you don't need this constant monitoring, so an easy fix is to use BindingMode.OneTime:
<DataTemplate DataType="{x:Type system:String}">
<TextBlock Text="{Binding Content.Length, RelativeSource={RelativeSource AncestorType=ContentControl}, Mode=OneTime}" />
</DataTemplate>
Is it possible to add and bind user controls dynamically? Maybe I'll show sample code to show what I exactly mean.
MainWindow:
<UniformGrid
Rows="11"
Columns="11"
DataContext="{StaticResource vm}">
<local:DynamicUserControl
ButClickControl="{Binding Path=UserControlObjects[0].ButClickCommand}"
SomeDataInUserControl="{Binding Path=UserControlObjects[0].SomeData, Mode=OneWay}" />
<local:DynamicUserControl
ButClickControl="{Binding Path=UserControlObjects[1].ButClickCommand}"
SomeDataInUserControl="{Binding Path=UserControlObjects[1].SomeData, Mode=OneWay}" />
<local:DynamicUserControl
ButClickControl="{Binding Path=UserControlObjects[2].ButClickCommand}"
SomeDataInUserControl="{Binding Path=UserControlObjects[2].SomeData, Mode=OneWay}" />
.....
</UniformGrid>
In ViewModel there is an array of UserControlObjects. But in this array I will have over 100 elements, so it is not the best option to write all elements one by one. Is there any way to add DynamicUserControls not in XAML but somewhere in code in loop with keeping the MVVM pattern and binding?
Use an ItemsControl with the UniformGrid as ItemsPanel and the DynamicUserControl in the ItemTemplate:
<ItemsControl DataContext="{StaticResource vm}"
ItemsSource="{Binding UserControlObjects}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Rows="11" Columns="11"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:DynamicUserControl
ButClickControl="{Binding ButClickCommand}"
SomeDataInUserControl="{Binding SomeData}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
In my opinion, you would want to keep any controls out of your view model. You could however keep the individual view models that back the controls in a list within the main view model. For example, create the view model that will provide the data for the “dynamic” controls.
class SubViewModel
{
public string Name { get; private set; } = string.Empty;
public SubViewModel(string aName)
{
Name = aName;
}
}
And in the main view model you can do whatever you would do to dynamically create instances. In this case, I am just creating then in a for loop.
class MainWindowViewModel
{
public ObservableCollection<SubViewModel> SubViewModels
{
get
{
return mSubViewModels;
}
} private ObservableCollection<SubViewModel> mSubViewModels = new ObservableCollection<SubViewModel>();
public MainWindowViewModel()
{
for(int i = 0; i < 30; i++)
{
SubViewModels.Add(new SubViewModel($"Control: {i}"));
}
}
}
Then in the view, you can utilize an ItemsControl with an UniformGrid based ItemsPanelTemplate, and then whatever you want for the data template, whether you define it there explicitly, or make a user control (like your local:DynamicUserControl) to clean things up. In this sample, the data template it explicitly defined.
<Window x:Class="ListOfViewsSample.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:ListOfViewsSample"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Grid>
<ItemsControl ItemsSource="{Binding SubViewModels}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Background="LightGray" Margin="10">
<Label Content="{Binding Name}" Margin="5" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Window>
which results in the following:
If don’t want the multiple dynamic views to be the same, you can look into data template selectors to display something different based on the specified view model, but based in your question I think you were looking for a list of the same control/data. Hope this helps!
The usual way of doing this is:
Create an ItemsControl for the dynamic items you want to create
Override the ItemsPanel to whatever you need (UniformGrid in your case)
Bind it to a list of view models, with one view model per control
Define DataTemplates to map each view model type to its corresponding view type
<Window x:Class="App1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cust="clr-namespace:App1.Customers"
Title="Customers"
Height="525"
Width="525">
<Window.Resources>
<DataTemplate DataType="{x:Type cust:CustomerViewModel}">
<cust:CustomerView />
</DataTemplate>
</Window.Resources>
<Grid x:Name="MainContent">
<ContentControl Content="{Binding CurrentViewModel}" />
</Grid>
</Window>
I've declared the datatemplate above in xaml. Over time there could be 20 viewmodels and views the main window might need to know about. I'd rather pass responsibility for adding the datatemplate(s) to the resource dictionary to somewhere else. How can I achieve the above in c#? Just the bit which adds the datatemplate to the resource dictionary
I've created a custom control that I insert into my window with the following code
<controls:ListExpander Text="Class Diagrams"></controls:ListExpander>
The control in question contains several subcontrols, among others, a list. How can create the setup, so I can specify items that should be added to the list? Eventually I'm looking for the following architecture
<controls:ListExpander Text="Class Diagrams">
<SomeItem>data<SomeItem>
<SomeItem>data<SomeItem>
<SomeItem>data<SomeItem>
<SomeItem>data<SomeItem>
</controls:ListExpander>
in which case the SomeItem objects should be added to the list in the ListExpander:
<ListBox Name="lstItems" Background="LightGray">
<ListBox.Items>
// Items should go here
</ListBox.Items>
</ListBox>
I'm quite new to WPF, but I suppose it's something along the lines of creating a dependency collection on ListExpander that takes object of the type SomeItem?
Edit: Let me clarify a bit. I simply want to be able to give the control a few arguments which it can translate into items in the listbox contained within the the control.
Have you created your class for ListExpander? You can inherit all functionality of an ItemsControl very simply. Here is a sample class I created for you to model it after.
CLASS
using System.Windows.Controls;
namespace ListExpanderSample
{
public class ListExpander : ItemsControl
{
static ListExpander()
{
/* Required logic goes here */
}
}
}
IMPLEMENTATION
<Grid>
<Grid.Resources>
<Style x:Key="{x:Type local:ListExpander}" TargetType="{x:Type local:ListExpander}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ListExpander}">
<Border Background="Transparent" >
<ScrollViewer Focusable="False" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<ItemsPresenter />
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Grid.Resources>
<local:ListExpander>
<local:ListExpander.Items>
<Button Content="Button 1"/>
<TextBox Text="TextBox 1"/>
<Button Content="Button 2"/>
<TextBox Text="TextBox 2"/>
</local:ListExpander.Items>
</local:ListExpander>
</Grid>
SCREENSHOT
Sample ListExpander http://lh4.ggpht.com/_jvamiP47SsA/SsWM6xN_3BI/AAAAAAAAAkg/tZUWxzi9wVI/s800/Window1.jpg
I hope this helped you out, Qua.
I assume your control contains more than just these children you are looking to add, right? So I would go by adding a property to your control class like this:
private ObservableCollection<UIElement> _items = new ObservableCollection<UIElement>();
public ObservableCollection<UIElement> Items
{
get { return this._items; }
}
Then in your controls template you just bind your listbox to this collection
<ListBox Name="lstItems" Background="LightGray" ItemsSource="{Binding Items}">
</ListBox>
This way you'll be able to use it like this:
<controls:ListExpander Text="Class Diagrams">
<controls:ListExpander.Items>
<SomeItem>data<SomeItem>
<SomeItem>data<SomeItem>
<SomeItem>data<SomeItem>
<SomeItem>data<SomeItem>
</controls:ListExpander.Items>
</controls:ListExpander>
To be able to use it without the <controls:ListExpander.Items> decorate your class with [ContentProperty("Items")] attribute.
There's a simple way to do it. Add a property of type ItemCollection to your control class like this:
public ItemCollection Items{
get{
return innerItems.Items;
}set{
innerItems.Items.Clear();
foreach (var item in value){
innerItems.Items.Add(item);
}
}
}
where innerItems is just an ItemsControl in your XAML (like this):
<UserControl x:Class="TestApp.ItemsExtender"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="300" Width="300">
<StackPanel>
<Label Content="Control Label" />
<ItemsControl x:Name="innerItems" />
</StackPanel>
</UserControl>
then add an attribute to your control class like this:
[ContentProperty("Items")]
public partial class ItemsExtender : UserControl
{...}
this will tell XAML that default items should be assigned to your Items property, and your Items property will use them to populate your inner list collection. You can then consume it like this:
<this:ItemsExtender>
<Label Content="item1" />
<Label Content="item2" />
<Label Content="item3" />
</this:ItemsExtender>
You can extend this sample with type converters to support adding arbitrary things to the collection and dependency properties to support binding, but this should get you started.
Hope it helps!