How to add new user control in TabControl.ContentTemplate? - wpf

I am little stuck with adding new instances of a usercontrol in a TabControl.ContentTemplate?
My Xaml is here:
<TabControl ItemsSource="{Binding Tables}">
<TabControl.ItemTemplate>
<DataTemplate>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate DataType="{x:Type uc:mytest1}">
<uc:mytest1>
</uc:mytest1>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
I am binding TabControl.ItemsSource property to an ObservableCollection and in the content template I am adding a user control, but when this app runs I am getting new items as TabItems but the content page is holding same user control, but I want new user controls to be added for each new TabItem.
I am very new to the WPF and may be I am doing a very basic mistake, kindly guide me.

The ControlTemplate determines the appearance of the elements of the tab control that are not part of the individual tab items. The ItemTemplate handles the content of the individual tab items. Additionally, a TabItem is a headered content control, which means it has two content type properties Content and Header with two separate templates ContentTemplate and HeaderTemplate. In order to be able to populate the tab items using binding, you need to style the TabItem using the above properties.
Example:
<Window x:Class="Example.Window2"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Name="Window"
Title="Window2" Height="300" Width="300">
<Window.DataContext>
<Binding ElementName="Window" Path="VM"/>
</Window.DataContext>
<Window.Resources>
<DataTemplate x:Key="TabItemHeaderTemplate">
<Grid>
<TextBlock Text="{Binding Header}"/>
<Ellipse Fill="Red" Width="40" Height="40" Margin="0,20,0,0"/>
</Grid>
</DataTemplate>
<DataTemplate x:Key="TabItemContentTemplate">
<Ellipse Fill="Green"/>
</DataTemplate>
<Style x:Key="TabItemContainerStyle" TargetType="TabItem">
<Setter Property="Header" Value="{Binding}"/>
<Setter Property="HeaderTemplate"
Value="{StaticResource TabItemHeaderTemplate}"/>
<Setter Property="Content" Value="{Binding}"/>
<Setter Property="ContentTemplate"
Value="{StaticResource TabItemContentTemplate}"/>
</Style>
</Window.Resources>
<Grid>
<TabControl ItemsSource="{Binding Items}"
ItemContainerStyle="{StaticResource TabItemContainerStyle}"/>
</Grid>
</Window>
The code behind:
public partial class Window2 : Window
{
public TabControlVM VM { get; set; }
public Window2()
{
VM = new TabControlVM();
InitializeComponent();
}
}
And the view model classes:
public class TabControlVM
{
public ObservableCollection<TabItemVM> Items { get; set; }
public TabControlVM()
{
Items = new ObservableCollection<TabItemVM>();
Items.Add(new TabItemVM("tabitem1"));
Items.Add(new TabItemVM("tabitem2"));
Items.Add(new TabItemVM("tabitem3"));
Items.Add(new TabItemVM("tabitem4"));
}
}
public class TabItemVM
{
public string Header { get; set; }
public TabItemVM(string header)
{
Header = header;
}
}

Saurabh, When you set Template, usually DataTemplate, ControlTemplate etc, the visual elements inside these templates are reused in WPF with concept of UI Virtualization. TabControl typically displays only one item at a time, so it does not create new Visual Item for every tab item, instead it only changes that DataContext and refreshes bindings of "Selected Visual Item". Its loaded/unloaded events are fired, but the object is same always.
You can use loaded/unload events and write your code accordingly that your "Visual Element" which is your usercontrol, so that control should be stateless and is not dependent on old data. When new DataContext has applied you should refresh everything.
DataContextChanged, Loaded and Unloaded events can help you remove all dependencies on old data.
Otherwise, you an create a new TabItem manually with your UserControl as its Child and add it in TabControl instead of adding Data Items.
Adding TabItems manually will create new control for every item and in selected area different elements will appear based on selection.

Related

How to correctly bind to a dependency property of a usercontrol in a MVVM framework

I have been unable to find a clean, simple, example of how to correctly implement a usercontrol with WPF that has a DependencyProperty within the MVVM framework. My code below fails whenever I assign the usercontrol a DataContext.
I am trying to:
Set the DependencyProperty from the calling ItemsControl , and
Make the value of that DependencyProperty available to the ViewModel of the called usercontrol.
I still have a lot to learn and sincerely appreciate any help.
This is the ItemsControl in the topmost usercontrol that is making the call to the InkStringView usercontrol with the DependencyProperty TextInControl (example from another question).
<ItemsControl ItemsSource="{Binding Strings}" x:Name="self" >
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel HorizontalAlignment="Left" VerticalAlignment="Top" Orientation="Vertical" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<DataTemplate.Resources>
<Style TargetType="v:InkStringView">
<Setter Property="FontSize" Value="25"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
</Style>
</DataTemplate.Resources>
<v:InkStringView TextInControl="{Binding text, ElementName=self}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Here is the InkStringView usercontrol with the DependencyProperty.
XAML:
<UserControl x:Class="Nova5.UI.Views.Ink.InkStringView"
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"
x:Name="mainInkStringView"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{Binding TextInControl, ElementName=mainInkStringView}" />
<TextBlock Grid.Row="1" Text="I am row 1" />
</Grid>
</UserControl>
Code-Behind file:
namespace Nova5.UI.Views.Ink
{
public partial class InkStringView : UserControl
{
public InkStringView()
{
InitializeComponent();
this.DataContext = new InkStringViewModel(); <--THIS PREVENTS CORRECT BINDING, WHAT
} --ELSE TO DO?????
public String TextInControl
{
get { return (String)GetValue(TextInControlProperty); }
set { SetValue(TextInControlProperty, value); }
}
public static readonly DependencyProperty TextInControlProperty =
DependencyProperty.Register("TextInControl", typeof(String), typeof(InkStringView));
}
}
That is one of the many reasons you should never set the DataContext directly from the UserControl itself.
When you do so, you can no longer use any other DataContext with it because the UserControl's DataContext is hardcoded to an instance that only the UserControl has access to, which kind of defeats one of WPF's biggest advantages of having separate UI and data layers.
There are two main ways of using UserControls in WPF
A standalone UserControl that can be used anywhere without a specific DataContext being required.
This type of UserControl normally exposes DependencyProperties for any values it needs, and would be used like this:
<v:InkStringView TextInControl="{Binding SomeValue}" />
Typical examples I can think of would be anything generic such as a Calendar control or Popup control.
A UserControl that is meant to be used with a specific Model or ViewModel only.
These UserControls are far more common for me, and is probably what you are looking for in your case. An example of how I would use such a UserControl would be this:
<v:InkStringView DataContext="{Binding MyInkStringViewModelProperty}" />
Or more frequently, it would be used with an implicit DataTemplate. An implicit DataTemplate is a DataTemplate with a DataType and no Key, and WPF will automatically use this template anytime it wants to render an object of the specified type.
<Window.Resources>
<DataTemplate DataType="{x:Type m:InkStringViewModel}">
<v:InkStringView />
</DataTemplate>
<Window.Resources>
<!-- Binding to a single ViewModel -->
<ContentPresenter Content="{Binding MyInkStringViewModelProperty}" />
<!-- Binding to a collection of ViewModels -->
<ItemsControl ItemsSource="{Binding MyCollectionOfInkStringViewModels}" />
No ContentPresenter.ItemTemplate or ItemsControl.ItemTemplate is needed when using this method.
Don't mix these two methods up, it doesn't go well :)
But anyways, to explain your specific problem in a bit more detail
When you create your UserControl like this
<v:InkStringView TextInControl="{Binding text}" />
you are basically saying
var vw = new InkStringView()
vw.TextInControl = vw.DataContext.text;
vw.DataContext is not specified anywhere in the XAML, so it gets inherited from the parent item, which results in
vw.DataContext = Strings[x];
so your binding that sets TextInControl = vw.DataContext.text is valid and resolves just fine at runtime.
However when you run this in your UserControl constructor
this.DataContext = new InkStringViewModel();
the DataContext is set to a value, so no longer gets automatically inherited from the parent.
So now the code that gets run looks like this:
var vw = new InkStringView()
vw.DataContext = new InkStringViewModel();
vw.TextInControl = vw.DataContext.text;
and naturally, InkStringViewModel does not have a property called text, so the binding fails at runtime.
You're almost there. The problem is that you're creating a ViewModel for your UserControl. This is a smell.
UserControls should look and behave just like any other control, as viewed from the outside. You correctly have exposed properties on the control, and are binding inner controls to these properties. That's all correct.
Where you fail is trying to create a ViewModel for everything. So ditch that stupid InkStringViewModel and let whoever is using the control to bind their view model to it.
If you are tempted to ask "what about the logic in the view model? If I get rid of it I'll have to put code in the codebehind!" I answer, "is it business logic? That shouldn't be embedded in your UserControl anyhow. And MVVM != no codebehind. Use codebehind for your UI logic. It's where it belongs."
Seems like you are mixing the model of the parent view with the model of the UC.
Here is a sample that matches your code:
The MainViewModel:
using System.Collections.Generic;
namespace UCItemsControl
{
public class MyString
{
public string text { get; set; }
}
public class MainViewModel
{
public ObservableCollection<MyString> Strings { get; set; }
public MainViewModel()
{
Strings = new ObservableCollection<MyString>
{
new MyString{ text = "First" },
new MyString{ text = "Second" },
new MyString{ text = "Third" }
};
}
}
}
The MainWindow that uses it:
<Window x:Class="UCItemsControl.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:v="clr-namespace:UCItemsControl"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<v:MainViewModel></v:MainViewModel>
</Window.DataContext>
<Grid>
<ItemsControl
ItemsSource="{Binding Strings}" x:Name="self" >
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel HorizontalAlignment="Left" VerticalAlignment="Top" Orientation="Vertical" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<DataTemplate.Resources>
<Style TargetType="v:InkStringView">
<Setter Property="FontSize" Value="25"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
</Style>
</DataTemplate.Resources>
<v:InkStringView TextInControl="{Binding text}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Window>
Your UC (no set of DataContext):
public partial class InkStringView : UserControl
{
public InkStringView()
{
InitializeComponent();
}
public String TextInControl
{
get { return (String)GetValue(TextInControlProperty); }
set { SetValue(TextInControlProperty, value); }
}
public static readonly DependencyProperty TextInControlProperty =
DependencyProperty.Register("TextInControl", typeof(String), typeof(InkStringView));
}
(Your XAML is OK)
With that I can obtain what I guess is the expected result, a list of values:
First
I am row 1
Second
I am row 1
Third
I am row 1
You need to do 2 things here (I'm assuming Strings is an ObservableCollection<string>).
1) Remove this.DataContext = new InkStringViewModel(); from the InkStringView constructor. The DataContext will be one element of the Strings ObservableCollection.
2) Change
<v:InkStringView TextInControl="{Binding text, ElementName=self}" />
to
<v:InkStringView TextInControl="{Binding }" />
The xaml you have is looking for a "Text" property on the ItemsControl to bind the value TextInControl to. The xaml I put using the DataContext (which happens to be a string) to bind TextInControl to. If Strings is actually an ObservableCollection with a string Property of SomeProperty that you want to bind to then change it to this instead.
<v:InkStringView TextInControl="{Binding SomeProperty}" />

Load controls on runtime based on selection

I'm new to XAML and I have a case where I need to change controls based on a selection on a combobox with templates.
For example, let's say that a user selects a template that requires a day of week and a time range that something will be available. I would like that, on the moment of the selection, the control with the information needed get build on the screen and that the bindings get to work as well.
Can someone give me a hint or indicate an article with an elegant way to do so?
Thanks in advance.
The solution you are looking for is a ContentControl and DataTemplates. You use the selected item of the ComboBox to change ContentTemplate of the Content Control.
You question mentions binding so I will assume you understand the MVVM pattern.
As an example, lets use MyModel1 as the Model
public class MyModel1
{
private Collection<string> values;
public Collection<string> Values { get { return values ?? (values = new Collection<string> { "One", "Two" }); } }
public string Field1 { get; set; }
public string Field2 { get; set; }
}
And MyViewModel as the ViewModel
public class MyViewModel
{
public MyViewModel()
{
Model = new MyModel1();
}
public MyModel1 Model { get; set; }
}
And the code behind does nothing but instantiate the ViewModel.
public partial class MainWindow : Window
{
public MainWindow()
{
ViewModel = new MyViewModel();
InitializeComponent();
}
public MyViewModel ViewModel { get; set; }
}
All three are very simple classes. The fun comes in the Xaml which is
<Window x:Class="StackOverflow._20893945.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:system="clr-namespace:System;assembly=mscorlib"
xmlns:this="clr-namespace:StackOverflow._20893945"
DataContext="{Binding RelativeSource={RelativeSource Self}, Path=ViewModel}"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<DataTemplate x:Key="MyModel1Template1" DataType="{x:Type this:MyModel1}">
<StackPanel>
<TextBlock Text="Template 1"></TextBlock>
<ComboBox ItemsSource="{Binding Path=Values}" SelectedItem="{Binding Path=Field1}" />
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="MyModel1Template2" DataType="{x:Type this:MyModel1}">
<StackPanel>
<TextBlock Text="Template 2"></TextBlock>
<TextBox Text="{Binding Path=Field2}" />
</StackPanel>
</DataTemplate>
</Window.Resources>
<DockPanel>
<StackPanel Orientation="Horizontal" DockPanel.Dock="Top" Margin="2">
<ComboBox x:Name="TypeSelector">
<system:String>Template 1</system:String>
<system:String>Template 2</system:String>
</ComboBox>
</StackPanel>
<ContentControl Content="{Binding Path=Model}">
<ContentControl.Style>
<Style TargetType="{x:Type ContentControl}">
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=TypeSelector, Path=SelectedItem}" Value="Template 2">
<Setter Property="ContentTemplate" Value="{StaticResource MyModel1Template2}" />
</DataTrigger>
</Style.Triggers>
<Setter Property="ContentTemplate" Value="{StaticResource MyModel1Template1}" />
</Style>
</ContentControl.Style>
</ContentControl>
</DockPanel>
</Window>
The notable points of the view are
The DataContext is initialised on the Window element, allowing for auto-complete on our binding expressions
The definition of 2 template to display 2 different views of the data.
The ComboBox is populated with a list of strings and has a default selection of the first element.
The ContentControl has its content bound to the Model exposed via the ViewModel
The default DataTemplate is the first template with a ComboBox.
The Trigger in the ContentControl's style will change the ContentTemplate if the SelectedItem of the ComboBox is changed to 'Template 2'
Implied facts are
If the SelectedItem changes back to 'Template 1', the style will revert the the ContentTemplate back to the default, ie MyModel1Template1
If there were a need for 3 separate displays, create another DataTemplate, add a string to the ComboBox and add another DataTrigger.
NOTE: This is the complete source to my example. Create a new C#/WPF project with the same classes and past the code in. It should work.
I hope this helps.

How to loop through ItemsControl in WPF?

How can I loop through this ItemsControl and change it's TextBlock background in this Xaml's code behind page on some mouse event. I am new to WPF.
<ItemsControl ItemsSource="{Binding Path= HeaderList}" Name="Headers">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Name="Data" Text="{Binding }" Width="100" HorizontalAlignment="Left" PreviewMouseLeftButtonDown="MouseLeftButtonDown_Handler"
MouseEnter="MouseEnter_Handler" MouseLeave="MouseLeave_Handler">
</TextBlock>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Thanks in advance!!
Actually my requirement is to change individual TextBlock's background color on different mouse events. So i need to get access of TextBlock in code behind and depending upon login I can change that Textblock's background color accordingly. So i think need to iterate ItemsControl. in case if I bind Background Property then all on property change would have effect on all the Textblocks in that ItemsControl. I don't want it in this way. I want to set and change every individual textblock's color differently.
I have access to single one in the eventhandlers that caused that event, but I want to access all the textblocks that are in itemscontrol and change their color acoording to some logic
Solution with background binding like axelle suggested:
You can iterate through the items in the HeaderList and set the background-property.
The Header class must implement the INotifyPropertyChanged Interface
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1" x:Class="WpfApplication1.MainWindow"
Title="MainWindow" Height="350" Width="525">
<ItemsControl ItemsSource="{Binding Path=HeaderList}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Text}" Background="{Binding Background}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
public partial class MainWindow : Window
{
public class Header : NotificationObject
{
public string Text { get; set; }
public Brush Background { get; set; }
}
public IList<Header> HeaderList { get; set; }
public MainWindow()
{
HeaderList = new List<Header>
{
new Header {Text = "header1", Background = Brushes.Red},
new Header {Text = "header2", Background = Brushes.Blue},
new Header {Text = "header3", Background = Brushes.Chartreuse},
};
DataContext = this;
InitializeComponent();
}
}
If I understand your question correctly, you'd want to bind the TextBlock background to a value in your datacontext, and change that value on your mouse event.
don't loop through the itemscontrol, better use a Trigger to apply the changes to your textblock :)
<ItemsControl ItemsSource="{Binding Path= HeaderList}" Name="Headers">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="Background" Value="Red" />
</Trigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</DataTemplate>
</ItemsControl.ItemTemplate>

ContextMenu on ListBox Item with DataTemplate

I have a ListBox with different classes of items. DataTemplates are used to present those objects in the appropriate way. I want to have different context menus in the DataTemplates of these classes.
Everything works fine using the mouse, but using the keyboard I can't bring up the context menu.
This is probably because the keyboard-focus is not on the contents of the DataTemplate, but on the ListBoxItem.
How can I get the ListBoxItem to refer to the Content's ContextMenu?
Sample code:
<Window x:Class="WpfApplication8.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:my="clr-namespace:WpfApplication8"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<DataTemplate DataType="{x:Type my:Orange}">
<TextBlock>
Orange
<TextBlock.ContextMenu>
<ContextMenu>
<MenuItem Header="Peel"/>
</ContextMenu>
</TextBlock.ContextMenu>
</TextBlock>
</DataTemplate>
<DataTemplate DataType="{x:Type my:Apple}">
<TextBlock>
Apple
<TextBlock.ContextMenu>
<ContextMenu>
<MenuItem Header="Uncore"/>
</ContextMenu>
</TextBlock.ContextMenu>
</TextBlock>
</DataTemplate>
</Window.Resources>
<Grid>
<ListBox ItemsSource="{Binding Fruits}"/>
</Grid>
</Window>
using System.Windows;
using System.Collections.ObjectModel;
namespace WpfApplication8
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Fruits = new ObservableCollection<Fruit>();
Fruits.Add(new Apple());
Fruits.Add(new Apple());
Fruits.Add(new Orange());
this.DataContext = this;
}
public ObservableCollection<Fruit> Fruits { get; set; }
}
public class Fruit
{
}
public class Apple : Fruit
{
}
public class Orange : Fruit
{
}
}
I too had this problem. Reading Bea Stollnitz' blog gave me an idea.
I started with a data template like this in my resources:
<ContextMenu x:Key="MyMenu">
<MenuItem Header="A" />
<MenuItem Header="B" />
<MenuItem Header="C" />
</ContextMenu>
<DataTemplate x:Key="MyTemplateKey" DataType="{x:Type local:myType}">
<TextBlock ContextMenu="{StaticResource MyMenu}" >
<Run Text="{Binding Path=MyBindingPath}" FontSize="20" FontWeight="Bold" />
</TextBlock>
</DataTemplate>
As described above, this causes the keyboard menu key not to invoke the context menu, although right clicking does work. The problem is the context menu needs to be on the ListBoxItem, not the template inside.
Hey presto!
<Style x:Key="ContextLBI" TargetType="{x:Type ListBoxItem}">
<Setter Property="ContextMenu" Value="{StaticResource MyMenu}">
</Setter>
</Style>
Now, just remove the ContextMenu from the data template, and set your style on your list box like this:
<ListBox ItemTemplate="{StaticResource MyTemplateKey}"
ItemContainerStyle="{StaticResource ContextLBI}"
... >
</ListBox>
This guy have similar problem as you: http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/5737a331-2014-4e39-b87c-215ae6a7cdd4.
Instead of fighting with focus, add a context menu to the listbox. Add a ContextMenuOpening event handler to your listbox. In that handler, depending on data context of currently selected item, add whatever menuitems you need programmatically.
I found a solution. In the code-behind I will give each ListBoxItem the context menu I find from its visual children.
It gives me the possibility of adding the context menus to the DataTemplates for the various class, thus giving me the polymorphism I like. I also prefer to declare the menus in XAML. And it works with keyboard navigation, as well as mouse use.
The code could probably have been put in an attached property or something for elegance.
I add a loaded event handler and this code:
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
foreach (var item in list.Items)
{
ListBoxItem lbItem = list.ItemContainerGenerator.ContainerFromItem(item) as ListBoxItem;
lbItem.ContextMenu = FindContextMenu(lbItem);
}
}
private ContextMenu FindContextMenu(DependencyObject depObj)
{
ContextMenu cm = depObj.GetValue(ContextMenuProperty) as ContextMenu;
if (cm != null)
return cm;
int children = VisualTreeHelper.GetChildrenCount(depObj);
for (int i = 0; i < children; i++)
{
cm = FindContextMenu(VisualTreeHelper.GetChild(depObj, i));
if(cm != null)
return cm;
}
return null;
}

Binding ContentControl to an ObservableCollection if Count == 1

how can I bind the Content of a ContentControl to an ObservableCollection.
The control should show an object as content only if the ObservableColelction contains exactly one object (the object to be shown).
Thanks,
Walter
This is easy. Just use this DataTemplate:
<DataTemplate x:Key="ShowItemIfExactlyOneItem">
<ItemsControl x:Name="ic">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate><Grid/></ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Count}" Value="1">
<Setter TargetName="ic" Property="ItemsSource" Value="{Binding}" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
This is used as the ContentTemplate of your ContentControl. For example:
<Button Content="{Binding observableCollection}"
ContentTemplate="{StaticResource ShowItemIfExactlyOneItem}" />
That's all you need to do.
How it works: The template normally contains an ItemsControl with no items, which is invisible and has no size. But if the ObservableCollection that is set as Content ever has exactly one item in it (Count==1), the trigger fires and sets the ItemsSource of the ItmesControl, causing the single item to display using a Grid for a panel. The Grid template is required because the default panel (StackPanel) does not allow its content to expand to fill the available space.
Note: If you also want to specify a DataTemplate for the item itself rather than using the default template, set the "ItemTemplate" property of the ItemsControl.
+1, Good question :)
You can bind the ContentControl to an ObservableCollection<T> and WPF is smart enough to know that you are only interested in rendering one item from the collection (the 'current' item)
(Aside: this is the basis of master-detail collections in WPF, bind an ItemsControl and a ContentControl to the same collection, and set the IsSynchronizedWithCurrentItem=True on the ItemsControl)
Your question, though, asks how to render the content only if the collection contains a single item... for this, we need to utilize the fact that ObservableCollection<T> contains a public Count property, and some judicious use of DataTriggers...
Try this...
First, here's my trivial Model object, 'Customer'
public class Customer
{
public string Name { get; set; }
}
Now, a ViewModel that exposes a collection of these objects...
public class ViewModel
{
public ViewModel()
{
MyCollection = new ObservableCollection<Customer>();
// Add and remove items to check that the DataTrigger fires correctly...
MyCollection.Add(new Customer { Name = "John Smith" });
//MyCollection.Add(new Customer { Name = "Mary Smith" });
}
public ObservableCollection<Customer> MyCollection { get; private set; }
}
Set the DataContext in the Window to be an instance of the VM...
public Window1()
{
InitializeComponent();
this.DataContext = new ViewModel();
}
and here's the fun bit: the XAML to template a Customer object, and set a DataTrigger to remove the 'Invalid Count' part if (and only if) the Count is equal to 1.
<Window x:Class="WpfApplication1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1"
Title="Window1" Height="300" Width="300">
<Window.Resources>
<Style TargetType="{x:Type ContentControl}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate x:Name="template">
<Grid>
<Grid Background="AliceBlue">
<TextBlock Text="{Binding Name}" />
</Grid>
<Grid x:Name="invalidCountGrid" Background="LightGray" Visibility="Visible">
<TextBlock
VerticalAlignment="Center" HorizontalAlignment="Center"
Text="Invalid Count" />
</Grid>
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Count}" Value="1">
<Setter TargetName="invalidCountGrid" Property="Visibility" Value="Collapsed" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<ContentControl
Margin="30"
Content="{Binding MyCollection}" />
</Window>
UPDATE
To get this dynamic behaviour working, there is another class that will help us... the CollectionViewSource
Update your VM to expose an ICollectionView, like:
public class ViewModel
{
public ViewModel()
{
MyCollection = new ObservableCollection<Customer>();
CollectionView = CollectionViewSource.GetDefaultView(MyCollection);
}
public ObservableCollection<Customer> MyCollection { get; private set; }
public ICollectionView CollectionView { get; private set; }
internal void Add(Customer customer)
{
MyCollection.Add(customer);
CollectionView.MoveCurrentTo(customer);
}
}
And in the Window wire a button Click event up to the new 'Add' method (You can use Commanding if you prefer, this is just as effective for now)
private void Button_Click(object sender, RoutedEventArgs e)
{
_viewModel.Add(new Customer { Name = "John Smith" });
}
Then in the XAML, without changing the Resource at all - make this the body of your Window:
<StackPanel>
<TextBlock Height="20">
<TextBlock.Text>
<MultiBinding StringFormat="{}Count: {0}">
<Binding Path="MyCollection.Count" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<Button Click="Button_Click" Width="80">Add</Button>
<ContentControl
Margin="30" Height="120"
Content="{Binding CollectionView}" />
</StackPanel>
So now, the Content of your ContentControl is the ICollectionView, and you can tell WPF what the current item is, using the MoveCurrentTo() method.
Note that, even though ICollectionView does not itself contain properties called 'Count' or 'Name', the platform is smart enough to use the underlying data source from the CollectionView in our Bindings...

Resources