How to use a trigger to affect related grid elements? - wpf

As an exercise in WPF I am dabbling with a Sudoku-like grid.
Consider the following (simplified) example
XAML
<Window x:Class="SO_WPF_Question_Sample.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:SO_WPF_Question_Sample"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<Style x:Key="MouseOverStyle" TargetType="{x:Type Label}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="Red"/>
<!-- I want something to happen here -->
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<ItemsControl ItemsSource="{Binding Path=Items}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="Black" BorderThickness="1,1,1,1">
<Label Content="{Binding}"
Style="{StaticResource MouseOverStyle}"
/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Window>
Code-behind
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainWindowViewModel();
}
}
Viewmodel
public class MainWindowViewModel // INotifyPropertyChanged omitted for simplicity
{
public MainWindowViewModel()
{
PopulateElements(4);
}
public IEnumerable<Cell> Items { get; set; }
private void PopulateElements(int size)
{
int div = (int)Math.Sqrt(size);
IList<Cell> items = new List<Cell>();
for (int i = 0; i < size; i++)
{
for (int j = 0; j < size; j++)
{
items.Add(new Cell()
{
X = i,
Y = j,
Z = j / div + div * (i / div)
});
}
}
Items = items;
}
}
POCO class
public class Cell
{
public int X { get; set; }
public int Y { get; set; }
public int Z { get; set; }
public override string ToString()
{
return "Row: " + X + " Col: " + Y + " Box: " + Z;
}
}
The only interesting bit: whenever I hover over a grid element, it turns red.
What I want: whenever I hover over a grid element, I want to highlight all 'sibling' cells as well. I.e. the ones where X, Y and Z are equal to those of the active cell.
I don't know how to achieve that, but I can think about some strategies.
Active strategy:
Upon the Trigger, somehow fire some method (something like a Command?) (or something else?), have that parse all Cells in the grid, if they are siblings, set some property on them, and define a Style to respond to a DataTrigger.
Passive strategy:
Upon the trigger, set some ViewModel property that triggers an event that the Cells subscribe to (INotifyPropertChanged springs to mind), have an event handler in Cell set a property and again have a Style with a DataTrigger. I can see this work in my mind but there is a coupling issue with this approach. Also, how would I convey the info about the grid element being hovered over?
While researching this I came across EventTrigger and Interaction but those seem to be primarily geared to animations. There is probably a better/simpler approach to this. I have the feeling I am overthinking this.
I would appreciate advice nudging me to an approach on how to tackle this and if possible why that approach would be favorable.
Update it turns out that what my question really boiled down to was how to call a Command (with a CommandParameter) using EventTrigger. There are a lot of answers on SO on this one, a lot of them involving MVVM Light. I solved it by using Microsoft.Xaml.Behaviors.Wpf and can now see how MVVM Light might have been the better/easier way to go.

From experience alone I'm biased towards something along the lines of the first proposed solution.
Add the following namespaces to the xaml:
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:cmd="http://www.galasoft.ch/mvvmlight"
And then use EventToCommand:
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseEnter">
<cmd:EventToCommand Command="{Binding HoverCellCommand}" PassEventArgsToCommand="True"/>
</i:EventTrigger>
</i:Interaction.Triggers>
In the view model you'd have something like...
public RelayCommand<MouseEventArgs> HoverCellCommand {get; private set;}
HoverCellCommand = new RelayCommand<MouseEventArgs>(p =>
{
//find cell siblings, etc...
});

For future reference for myself, this is how I did it without using MVVM Light. Instead, I installed the Microsoft.Xaml.Behaviors.Wpf NuGet package to get the Behaviors stuff (known as Interactivity in Blend)
First the basic stuff: create a command:
public class ChangeSiblingColorCommand : ICommand
{
private readonly Action<Cell> _action;
#pragma warning disable CS0067 // The event is never used
public event EventHandler CanExecuteChanged;
#pragma warning restore CS0067 // The event is never used
public ChangeSiblingColorCommand(Action<Cell> action)
{
_action = action;
}
public bool CanExecute(object parameter)
{
return (parameter is Cell c);
}
public void Execute(object parameter)
{
_action((Cell)parameter);
}
}
Then include the command in the viewmodel so it can be bound to:
public MainWindowViewModel()
{
this.ChangeSiblingColorCommand = new ChangeSiblingColorCommand(HighlightSiblings);
}
public ICommand ChangeSiblingColorCommand { get; private set; }
private void HighlightSiblings(Cell cell)
{
var siblings = cell.Siblings(Items);
foreach (Cell c in siblings)
{
c.IsSiblingHighlighted = !c.IsSiblingHighlighted;
}
}
Finally, hook the command up in the view using the following rather scary XAML:
<ItemsControl ItemsSource="{Binding Path=Items}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Label Style="{StaticResource MouseOverStyle}" Content="{Binding Mode=OneWay}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseEnter">
<i:InvokeCommandAction Command="{Binding RelativeSource={RelativeSource AncestorType=UniformGrid,
Mode=FindAncestor},
Path=DataContext.ChangeSiblingColorCommand }"
CommandParameter="{Binding}"/>
</i:EventTrigger>
<i:EventTrigger EventName="MouseLeave">
<i:InvokeCommandAction Command="{Binding RelativeSource={RelativeSource AncestorType=UniformGrid,
Mode=FindAncestor},
Path=DataContext.ChangeSiblingColorCommand }"
CommandParameter="{Binding}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Label>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Be sure to include this reference in the view:
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"

Related

EventTrigger in WPF

I want to get the SelectedItems from the Listbox where the Checkbox is selected via the InvokeCommandAction and store them in an obsevableCollection SelectedItems , but I am not getting the SelectedItemChangedCommand working(break point does not hit) and not sure how do I populate the Items in the SelectedItems Collection. I tried following, hoping that once the checkbox is checked or unchecked, the SelectedItemChangedCommand would be called and I can have a method getting called on this where I will populate the SelectedItems
Please note I am looking for a way to achieve this without any code behind.
<ListBox Margin="45,7,0,0" VerticalAlignment="Top" Grid.Column="1" Grid.Row="0" Grid.RowSpan="2"
ItemsSource="{Binding ListItems}"
SelectionMode="Multiple" Height="146">
<ListBox.Resources>
<Style TargetType="ListBoxItem">
<Setter Property="OverridesDefaultStyle" Value="true" />
<Setter Property="SnapsToDevicePixels" Value="true" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<CheckBox Margin="5,2"
IsChecked="{TemplateBinding IsSelected}">
<ContentPresenter />
</CheckBox>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.Resources>
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction Command="{Binding SelectedItemChangedCommand}" CommandParameter="{Binding ElementName=myListBox, Path=SelectedItem}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</ListBox>
Updated Xaml File
<Window x:Class="stack.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:stack"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Grid>
<ListBox x:Name="myListBox" Margin="45,7,0,0" VerticalAlignment="Top"
ItemsSource="{Binding ListItems}"
SelectionMode="Multiple" Height="146">
<ListBox.Resources>
<Style TargetType="ListBoxItem">
<Setter Property="OverridesDefaultStyle" Value="true" />
<Setter Property="SnapsToDevicePixels" Value="true" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<CheckBox
Margin="5,2"
IsChecked="{Binding IsSelected, RelativeSource={RelativeSource TemplatedParent}}"
>
<ContentPresenter />
</CheckBox>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.Resources>
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction Command="{Binding SelectedItemChangedCommand}"
CommandParameter="{Binding ElementName=myListBox, Path=SelectedItem}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</ListBox>
</Grid>
</Window>
I am binding the listbox to observableCollection ListItems defined in view model as
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace stack
{
public class MainViewModel : INotifyPropertyChanged
{
public ObservableCollection<string> ListItems { get; set; }
public ObservableCollection<string> SelectedListItems { get; set; }
public RelayCommand SelectedItemChangedCommand { get; set; }
public string _selectedItem;
public string SelectedItem
{
get => _selectedItem;
set
{
_selectedItem = value;
OnPropertyChanged("SelectedItem");
}
}
public bool _isSelected;
public bool IsSelected
{
get => _isSelected;
set
{
_isSelected = value;
OnPropertyChanged("IsSelected");
}
}
public MainViewModel()
{
ListItems = new ObservableCollection<string>();
ListItems.Add("One");
ListItems.Add("Two");
ListItems.Add("three");
ListItems.Add("Four");
ListItems.Add("Five");
SelectedItemChangedCommand = new RelayCommand(this.ExecuteItemChanged);
}
public void ExecuteItemChanged(object parameter)
{
if (IsSelected)
{
SelectedListItems.Add(SelectedItem);
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler _propertyChangedEventHandler = PropertyChanged;
_propertyChangedEventHandler?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
TemplateBinding is cheap, but it doesn't do two-way bindings. Thus, the items aren't getting selected. You need a regular binding with a RelativeSource of TemplatedParent:
<CheckBox
Margin="5,2"
IsChecked="{Binding IsSelected, RelativeSource={RelativeSource TemplatedParent}}"
>
Note that you are binding CheckBox.IsChecked to ListBoxItem.IsSelected. The templated parent is a ListBoxItem, not your main viewmodel.
Second, you want to pass the SelectedItems, plural, property to your command. SelectedItem is singular. It's only one item. It'll be the topmost selected item when many are selected. You have SelectionMode="Multiple" on your listbox, so I assume you want the full selection.
<i:InvokeCommandAction
Command="{Binding SelectedItemChangedCommand}"
CommandParameter="{Binding ElementName=myListBox, Path=SelectedItems}"
/>
And make sure you have x:Name="myListBox" on the ListBox: The CommandParameter binding needs that to find SelectedItems.
Finally: ExecuteItemChanged() is very broken. Your main viewmodel properties IsSelected and SelectedItem are not bound to anything. They're just false and null, always. Every time the selection changes, you execute the command and pass in the first selected item as parameter, and then you ignore it and go look to see if false is still false, which it is. If it weren't, your viewmodel's SelectedItem property would still be null, because you never updated that either.
Here's what you want to do: When the selection changes, pass the entire collection of currently selected items into your command. Replace the viewmodel's entire collection of currently selected items with the current state from the control. You must, must, must bind SelectedItems as the CommandParameter above.
Get rid of SelectedItem and IsSelected on the viewmodel, they serve no purpose.
If possible, don't ever get into this business of maintaining two lists and trying to keep them in sync piecemeal. It's always a mess. And you don't need to do it in this case.
public void ExecuteItemChanged(object parameter)
{
// ListBox.SelectedItems is System.Windows.Controls.SelectedItemCollection,
// a precambrian monster that's declared internal in PresentationFramework.dll.
// However, it does implement non-generic IList, so cast it to that.
if (parameter is System.Collections.IList selectedItems)
{
if (SelectedListItems == null)
{
SelectedListItems = new ObservableCollection<String>();
}
SelectedListItems.Clear();
foreach (string item in selectedItems)
{
SelectedListItems.Add(item);
}
}
}

BarButtonItems and BarSubItems on Bound RibbonControl

I am developing a WPF application using DevExpress controls, such as the Ribbon control. I want to be able to place buttons on the ribbon dynamically. I would like to be able to support both regular buttons and drop-down buttons.
I was thinking something similar to below.
WPF View:
<UserControl.Resources>
<DataTemplate x:Key="RibbonCommandTemplate">
<ContentControl>
<dxb:BarButtonItem RibbonStyle="All" Content="{Binding Caption}"
Command="{Binding (dxr:RibbonControl.Ribbon).DataContext.MenuExecuteCommand,
RelativeSource={RelativeSource Self}}"
CommandParameter="{Binding}" />
</ContentControl>
</DataTemplate>
</UserControl.Resources>
<Grid>
<DockPanel>
<dxr:RibbonControl DockPanel.Dock="Top" RibbonStyle="Office2010">
<dxr:RibbonDefaultPageCategory>
<dxr:RibbonPage Caption="Home">
<dxr:RibbonPageGroup Caption="Dynamic Commands"
ItemLinksSource="{Binding DynamicCommands}"
ItemTemplate="{StaticResource RibbonCommandTemplate}" />
</dxr:RibbonPage>
</dxr:RibbonDefaultPageCategory>
</dxr:RibbonControl>
<Grid/>
</DockPanel>
</Grid>
View Model:
public class RibbonCommand
{
public string Caption { get; set; }
public int CommandCode { get; set; }
public ObservableCollection<RibbonCommand> SubItems { get; set; }
public bool HasSubItems
{
get
{
if (SubItems != null)
return (SubItems.Count > 0);
else
return false;
}
}
}
[POCOViewModel]
public class MainViewModel
{
public ObservableCollection<RibbonCommand> DynamicCommands { get; set; }
public MainViewModel()
{
DynamicCommands = new ObservableCollection<RibbonCommand>();
// Regular buttons.
DynamicCommands.Add(new RibbonCommand() { Caption = "Button 1", CommandCode = 1 });
DynamicCommands.Add(new RibbonCommand() { Caption = "Button 2", CommandCode = 2 });
// Drop-down button.
RibbonCommand dropDownCommand = new RibbonCommand() { Caption = "Drop-Down", CommandCode = 3 };
dropDownCommand.SubItems = new ObservableCollection<RibbonCommand>();
dropDownCommand.SubItems.Add(new RibbonCommand() { Caption = "Sub-Item 1", CommandCode = 31 });
dropDownCommand.SubItems.Add(new RibbonCommand() { Caption = "Sub-Item 2", CommandCode = 32 });
dropDownCommand.SubItems.Add(new RibbonCommand() { Caption = "Sub-Item 3", CommandCode = 33 });
DynamicCommands.Add(dropDownCommand);
}
public void MenuExecute(RibbonCommand command)
{
MessageBox.Show(string.Format("You clicked command with ID: {0} (\"{1}\").",
command.CommandCode, command.Caption), "Bound Ribbon Control");
}
}
This code does successfully populate the ribbon with items I added in my DynamicCommands collection, but I would like to support drop-down buttons for items with anything in the SubItems collection (the third button on my example above).
Is there a way to conditionally change the type of control displayed in a DataTemplate. If the object's HasSubItems is true, I would like a BarSubItem placed on the ribbon. If it is false, I will keep the BarButtonItem.
If this is regular WPF rather than UWP, and if the DataContexts of your subitems are of different types, you can define multiple DataTemplates with DataType attributes in the RibbonPageGroup's resources (where they won't be in scope for anything that doesn't need them), and get rid of that ItemTemplate attribute:
<dxr:RibbonPageGroup
Caption="Dynamic Commands"
ItemLinksSource="{Binding DynamicCommands}">
<dxr:RibbonPageGroup.Resources>
<DataTemplate DataType="{x:Type local:RibbonCommand}">
<!-- XAML stuff -->
</DataTemplate>
<DataTemplate DataType="{x:Type local:SpecialRibbonCommand}">
<!-- Totally different XAML stuff -->
</DataTemplate>
</dxr:RibbonPageGroup.Resources>
<!-- etc -->
For another option, you should be able to write a DataTemplateSelector and give it to the RibbonControl's ToolbarItemTemplateSelector property or the RibbonPageGroup's ItemTemplateSelector property.
Lastly, write one complicated DataTemplate with multiple child controls superimposed in a Grid, and a series of triggers that show only the appropriate one based on properties of the DataContext. If you've only got two different options to handle, this may be the quickest and easiest route.
<DataTemplate x:Key="RibbonCommandTemplate">
<Grid>
<Label x:Name="OneThing" />
<Label x:Name="AnotherThing" />
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding HasSubItems}" Value="True">
<Setter TargetName="OneThing" Property="Visibility" Value="Collapsed" />
<Setter TargetName="AnotherThing" Property="Visibility" Value="Visible" />
</DataTrigger>
<!-- Other triggers for HasSubItems == False, whatever -->
</DataTemplate.Triggers>
</DataTemplate>
This seems pretty crude, but I've done it so much in WPF that I'm getting desensitized to it.
I figured out a way to do this using a DataTemplateSelector class:
using System.Windows;
using System.Windows.Controls;
using RibbonDynamicButtons.ViewModels;
namespace RibbonDynamicButtons.Selectors
{
public class RibbonCommandSelector : DataTemplateSelector
{
public DataTemplate CommandTemplate { get; set; }
public DataTemplate SubCommandTemplate { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if(item is RibbonCommand)
{
RibbonCommand command = (RibbonCommand)item;
if (command.HasSubItems)
return SubCommandTemplate;
else
return CommandTemplate;
}
return base.SelectTemplate(item, container);
}
}
}
I added my selector to the xaml as follows:
<UserControl
...
xmlns:Selectors="clr-namespace:RibbonDynamicButtons.Selectors">
<UserControlResources>
<DataTemplate x:Key="RibbonSubItemTemplate">
<ContentControl>
<dxb:BarButtonItem RibbonStyle="SmallWithText" Content="{Binding Caption}"
Command="{Binding (dxr:RibbonControl.Ribbon).DataContext.MenuExecuteCommand,
RelativeSource={RelativeSource Self}}" CommandParameter="{Binding}" />
</ContentControl>
</DataTemplate>
<Selectors:RibbonCommandSelector x:Key="RibbonCommandSelector">
<Selectors:RibbonCommandSelector.CommandTemplate>
<DataTemplate>
<ContentControl>
<dxb:BarButtonItem RibbonStyle="All"
Content="{Binding Caption}"
Command="{Binding (dxr:RibbonControl.Ribbon).DataContext.MenuExecuteCommand,
RelativeSource={RelativeSource Self}}"
CommandParameter="{Binding}" />
</ContentControl>
</DataTemplate>
</Selectors:RibbonCommandSelector.CommandTemplate>
<Selectors:RibbonCommandSelector.SubCommandTemplate>
<DataTemplate>
<ContentControl>
<dxb:BarSubItem RibbonStyle="All" Content="{Binding Caption}"
ItemLinksSource="{Binding SubItems}"
ItemTemplate="{StaticResource RibbonSubItemTemplate}" />
</ContentControl>
</DataTemplate>
</Selectors:RibbonCommandSelector.SubCommandTemplate>
</Selectors:RibbonCommandSelector>
</UserControlResources>
I bind the ItemTemplateSelector to my selector on the RibbonPageGroup:
<dxr:RibbonPageGroup Caption="Dynamic Commands" ItemLinksSource="{Binding DynamicCommands}"
ItemTemplateSelector="{StaticResource RibbonCommandSelector}" />
I did not need to make any changes to the View Model I included on my original question.

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.

Using MVVM Light EventToCommand In DataTemplate

So I have a WPF UserControl:
<UserControl x:Class="BI_Builder.Views.ObjectTreeView"
x:Name="UC1"
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:local="clr-namespace:BI_Builder"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:viewModels="clr-namespace:BI_Builder.ViewModels"
xmlns:command="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.WPF4"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300" DataContext="{Binding}">
<UserControl.Resources>
<ContentControl x:Key="Context" Content="{Binding}" />
<DataTemplate x:Key="DataSourceTemplate">
<TextBlock Text="{Binding Path=Name}" >
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<command:EventToCommand Command="{Binding Path=DataContext.OpenCommand, Mode=OneWay,ElementName=UC1}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</TextBlock>
</DataTemplate>
<HierarchicalDataTemplate x:Key="ItemTemplate"
ItemsSource="{Binding Children}"
ItemTemplate="{StaticResource DataSourceTemplate}">
<StackPanel>
<TextBlock Text="{Binding Header}">
</TextBlock>
</StackPanel>
</HierarchicalDataTemplate>
</UserControl.Resources>
<Grid>
<TreeView Name="TreeView" ItemsSource="{Binding Items}" ItemTemplate="{StaticResource ItemTemplate}" >
</TreeView>
</Grid>
</UserControl>
And here's the main view model for the user control:
public class ObjectTreeViewModel : ObservableObject {
public ObservableCollection<ItemViewModel> Items {
get {
if (_items != null) return _items;
_items = new ObservableCollection<ItemViewModel>();
_items.Add(DataSources);
return _items;
}
set { _items = value;
}
}
public ItemViewModel DataSources {
get { return _dataSources ?? (_dataSources = new ItemViewModel() { Header = "Data Sources", Children = new ObservableCollection<object>(DataSourceList) }); }
set { _dataSources = value; }
}
public List<DataSource> DataSourceList;
public ICommand OpenCommand {
get { if (_openCommand == null) { return _openCommand = new RelayCommand(OpenDataSource); } return _openCommand; }
}
private void OpenDataSource() {
MessageBox.Show("Test");
}
public ObjectTreeViewModel() {
DataSourceList = new List<DataSource>();
DataSourceList.Add(new DataSource() { Name = "Test" });
}
private ItemViewModel _dataSources;
private ObservableCollection<ItemViewModel> _items;
private RelayCommand _openCommand;
}
}
I've tried every method I've come across on the web to get the EventToCommand in the DataSourceTemplate DataTemplate to fire. In fact, I'm pretty sure it knows where the OpenCommand is, because if I change the Path to gobbledygook, the Output window throws me an error saying that "ObjectTreeView" (which is the instance of the ObjectTreeViewModel view model being bound to the UserControl) doesn't have the gobbledygook property. So I think I've set the DataContext correctly ...
But whenever I click on the text blocks ... nothing.
Really trying to avoid code-behind (it just feels wrong), and full disclosure, I'm using MVVM Light's EventToCommand but not the full toolkit, although I'm tempted to rewrite what I have so far in it to see if using the Service Locator will solve this problem.
The TextBlock control does not have a Click event. See MSDN.
You should use the MouseLeftButtonDown event instead:
<i:EventTrigger EventName="MouseLeftButtonDown">
<!-- ... -->
</i:EventTrigger>
Can you put a hyperlink inside your textblock instead and bind the command to the hyperlink?
Note you can style the hyperlink to look like a plain textblock if needed.
<TextBlock>
<Hyperlink Command="{Binding Path=DataContext.OpenCommand" Text="{Binding Path=Name}" />
</TextBlock>
Also make sure that the ObjectTreeView class is instantiated and loaded into DataContext of the usercontrol.

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