TabControl view doesn't update on template change - wpf

I need to change the view of a TabControl's content on-the-fly.
I am guessing the best way to accomplish this is to define the view as a DataTemplate, and then change said template using a trigger.
In my test app, the background color is tied to the same data trigger as the template. The background color updates immediately upon making the radio button selection.
Expected behavior: The Tab Item Content / DataTemplate also updates immediately.
Actual Behavior: Tab content view does not update until the tab selection is changed.
Here's my Minimal, Complete, and Verifiable example:
Window XAML
<Window x:Class="ChangeView.Window1"
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"
Title="Window1" Height="350" Width="400">
<Window.Resources>
<DataTemplate x:Key="ContentTemplate1">
<Grid>
<Label HorizontalAlignment="Center" VerticalAlignment="Center" Content="{Binding MyBlurb}"/>
</Grid>
</DataTemplate>
<DataTemplate x:Key="ContentTemplate2">
<Grid>
<Label HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
Content="{Binding MyHeader}" Background="Black" Foreground="White" FontSize="72"/>
</Grid>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.Style>
<Style TargetType="{x:Type Grid}">
<Style.Triggers>
<DataTrigger Binding="{Binding ViewType1}" Value="False">
<Setter Property="Background" Value="Chartreuse"/>
</DataTrigger>
<DataTrigger Binding="{Binding ViewType1}" Value="True">
<Setter Property="Background" Value="Bisque"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Margin="10,38,0,0" Text="Content Template:"/>
<RadioButton x:Name="radio1" Margin="120,40,0,0" Grid.ColumnSpan="2" Content="1" GroupName="ViewSelect" IsChecked="{Binding Path=ViewType1}"/>
<RadioButton Margin="170,40,0,0" Grid.ColumnSpan="2" Content="2" GroupName="ViewSelect"/>
<TabControl Grid.Row="1" ItemsSource="{Binding TabGroup}">
<TabControl.Style>
<Style TargetType="{x:Type TabControl}">
<Setter Property="Margin" Value="10"/>
<Style.Triggers>
<DataTrigger Binding="{Binding ViewType1}" Value="True">
<Setter Property="ContentTemplate" Value="{DynamicResource ContentTemplate1}"/>
</DataTrigger>
<DataTrigger Binding="{Binding ViewType1}" Value="False">
<Setter Property="ContentTemplate" Value="{DynamicResource ContentTemplate2}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TabControl.Style>
<TabControl.ItemTemplate>
<DataTemplate>
<Border x:Name="headerBorder">
<Label Content="{Binding MyHeader}" FontSize="20"/>
</Border>
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
</Grid>
</Window>
Code Behind
namespace ChangeView
{
using System.Windows;
using System.ComponentModel;
using System.Collections.ObjectModel;
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : Window, INotifyPropertyChanged
{
public ObservableCollection<TabData> TabGroup { get; set; } = new ObservableCollection<TabData>();
private bool _viewType1 = true;
public bool ViewType1
{
get { return _viewType1; }
set { _viewType1 = value; RaisePropertyChanged(nameof(ViewType1)); }
}
public Window1()
{
TabGroup.Add(new TabData("♻️", "Recycle"));
TabGroup.Add(new TabData("⚔", "Swords"));
TabGroup.Add(new TabData("⚗", "Chemistry"));
TabGroup.Add(new TabData("🌵", "Cactus"));
TabGroup.Add(new TabData("👺", "Tengu"));
TabGroup.Add(new TabData("🐙", "Octopus"));
DataContext = this;
InitializeComponent();
}
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private void RaisePropertyChanged(string propName)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
public class TabData : INotifyPropertyChanged
{
private string _myHeader, _myBlurb;
public TabData(string header, string blurb)
{
MyHeader = header;
MyBlurb = blurb;
}
public string MyHeader
{
get { return _myHeader; }
set { _myHeader = value; RaisePropertyChanged(nameof(MyHeader)); }
}
public string MyBlurb
{
get { return _myBlurb; }
set { _myBlurb = value; RaisePropertyChanged(nameof(MyBlurb)); }
}
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private void RaisePropertyChanged(string propName)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
}

After changing the radio button state, change the selected tab. You will then see the correct content template.
It looks as if, in a TabControl, changing the content template alone does not cause the content to be rendered. If you render new content by switching the selected tab, the current content template will then be used.
So let's write one ContentTemplate, which creates a ContentControl and switches the ContentControl's ContentTemplate. I've tested, and the ContentControl will re-render its content when its ContentTemplate changes. The bindings get a little bit verbose.
<TabControl ItemsSource="{Binding TabGroup}" Grid.Row="1">
<TabControl.ContentTemplate>
<DataTemplate>
<ContentControl
x:Name="ContentCtl"
Content="{Binding}"
/>
<DataTemplate.Triggers>
<DataTrigger
Binding="{Binding DataContext.ViewType1, RelativeSource={RelativeSource AncestorType=TabControl}}"
Value="True">
<Setter
TargetName="ContentCtl"
Property="ContentTemplate"
Value="{DynamicResource ContentTemplate1}"
/>
</DataTrigger>
<DataTrigger
Binding="{Binding DataContext.ViewType1, RelativeSource={RelativeSource AncestorType=TabControl}}"
Value="False"
>
<Setter
TargetName="ContentCtl"
Property="ContentTemplate"
Value="{DynamicResource ContentTemplate2}"
/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</TabControl.ContentTemplate>
<TabControl.ItemTemplate>
<DataTemplate>
<Border x:Name="headerBorder">
<Label Content="{Binding MyHeader}" FontSize="20"/>
</Border>
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
You could also do something ugly in your code behind to make the TabControl render itself again on command. Or maybe you can replace the metadata on TabControl.ContentTemplate.

Related

EditorTemplate for TemplateColumn in XamGrid not working

I have a XamGrid with two columns, Name and Type. Depending on Type, I want to have a different kind of column for Name, thus I'm using a TemplateColumn. In the data template I have a ContentControl with a default ContentTemplate and a DataTrigger that sets the ContentTemplate to a different column style if Type is a specific value.
I am setting alll four templates (ItemTemplate, EditorTemplate, AddNewRowItemTemplate, AddNewRowEditorTemplate) of the TemplateColumn to this data template.
ItemTemplate, AddNewRowItemTemplate and AddNewRowEditorTemplate work as intended, however EditorTemplate does not, see attached pictures:
Here is my code:
MainWindow.xaml:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ig="http://schemas.infragistics.com/xaml"
Width="640" Height="480" >
<Window.Resources>
<DataTemplate x:Key="EditorTemplate">
<TextBox Width="64"/>
</DataTemplate>
<DataTemplate x:Key="BoolEditorTemplate">
<CheckBox/>
</DataTemplate>
<DataTemplate x:Key="DataTemplate">
<ContentControl Content="{Binding }">
<ContentControl.Style>
<Style TargetType="{x:Type ContentControl}">
<Setter Property="ContentTemplate" Value="{StaticResource EditorTemplate}" />
<Style.Triggers>
<DataTrigger Binding="{Binding Type}" Value="bool">
<Setter Property="ContentTemplate" Value="{StaticResource BoolEditorTemplate}" />
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</DataTemplate>
</Window.Resources>
<ig:XamGrid ItemsSource="{Binding DataCollection, RelativeSource={RelativeSource AncestorType=Window}}"
AutoGenerateColumns="False">
<ig:XamGrid.EditingSettings>
<ig:EditingSettings AllowEditing="Row" />
</ig:XamGrid.EditingSettings>
<ig:XamGrid.AddNewRowSettings>
<ig:AddNewRowSettings AllowAddNewRow="Top" />
</ig:XamGrid.AddNewRowSettings>
<ig:XamGrid.Columns>
<ig:TemplateColumn Key="Name"
ItemTemplate="{StaticResource DataTemplate}"
AddNewRowItemTemplate="{StaticResource DataTemplate}"
EditorTemplate="{StaticResource DataTemplate}"
AddNewRowEditorTemplate="{StaticResource DataTemplate}"/>
<ig:TextColumn Key="Type"/>
</ig:XamGrid.Columns>
</ig:XamGrid>
</Window>
MainWindow.xaml.cs:
using System.Collections.ObjectModel;
namespace WpfApplication1
{
public partial class MainWindow
{
public MainWindow()
{
InitializeComponent();
}
public ObservableCollection<Data> DataCollection { get; } = new ObservableCollection<Data>
{
new Data { Name = "Foo", Type = "bool" },
new Data { Name = "Bar", Type = "enum" }
};
}
public class Data
{
public string Name { get; set; }
public string Type { get; set; }
}
}
As explained here on the infragistics forum, for this use case not only is an EditorTemplate needed, but also an EditorStyle.
<Style x:Key="EditorStyle" TargetType="{x:Type ContentControl}">
<Setter Property="ContentTemplate" Value="{StaticResource EditorTemplate}" />
<Style.Triggers>
<DataTrigger Binding="{Binding Type}" Value="bool">
<Setter Property="ContentTemplate" Value="{StaticResource BoolEditorTemplate}" />
</DataTrigger>
</Style.Triggers>
</Style>
<DataTemplate x:Key="DataTemplate">
<ContentControl Content="{Binding }"
Style="{StaticResource EditorStyle}" />>
</DataTemplate>
[...]
<ig:TemplateColumn Key="Name"
ItemTemplate="{StaticResource DataTemplate}"
AddNewRowItemTemplate="{StaticResource DataTemplate}"
EditorTemplate="{StaticResource DataTemplate}"
AddNewRowEditorTemplate="{StaticResource DataTemplate}"
EditorStyle="{StaticResource EditorStyle}" />

WPF ContentControl content set in DataTemplate trigger randomly resets to default content

I'm having an issue with the content of a ContentControl whose content was set using DataTriggers randomly resetting the content to the default content specified in the DataTemplate.
The scenario is that I have a bunch of devices (sensors) out on the network that I need to check status on, among other things. Depending on what state the sensor is in I may want to show a colored circle (green, red or yellow), or an image. For example if the sensor is in use by someone I want to show an image representing a user. If the sensor is available for connection I want to show a green ellipse, etc.
I'm currently using a WPF DataGrid to display a list of sensors and their status, though I get the same erroneous behavior with a ListBox and ListView (haven't tried plain ItemsControl). FYI, sensors come and go asynchronously.
What you'll see if you run the sample code is that initially items with a connection state of CONNECTED will first display with the desired image. As rows get added to the grid the image randomly disappears and is replace with the default content specified in the DataTemplate. This problem only occurs when there is an image in the content. The other states work just fine.
Below is all the code (xaml, viewmodel, model) I think you'll need to see the behavior. Sorry for the amount of code posted below. I've tried to pair it down as much as possible to illustrate the issue. Hopefully the problem is obvious by looking at the XAML. The remaining source code will help you get it up and running more quickly if you so choose.
Here's the Window XAML:
<Window x:Class="StackOverflowGridIssue.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:model="clr-namespace:StackOverflowGridIssue.Model"
xmlns:shape="http://schemas.microsoft.com/expression/2010/drawing"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" Width="500"
Title="MainWindow" >
<Grid>
<DataGrid Name="_SensorsDataGrid" ItemsSource="{Binding Sensors}"
AutoGenerateColumns="False" HeadersVisibility="Column" >
<DataGrid.Columns>
<!-- Status -->
<DataGridTemplateColumn Header="Status" MinWidth="50"
Width="SizeToHeader" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ContentControl x:Name="myContent" Background="LimeGreen"
Width="25" Height="25">
<ContentControl.ToolTip>
<TextBlock Text="{Binding ConnectionState, Mode=OneWay}"
Foreground="Black" />
</ContentControl.ToolTip>
<ContentControl.Style>
<Style TargetType="{x:Type ContentControl}" BasedOn="{x:Null}" >
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ContentControl}">
<Grid>
<Ellipse Height="10" Width="10"
Fill="{TemplateBinding Background}"
Stroke="{TemplateBinding Background}" />
<ContentPresenter />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ContentControl.Style>
</ContentControl>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding ConnectionState, Mode=OneWay}"
Value="{x:Static model:ConnectionStateType.NOT_FOUND}">
<Setter TargetName="myContent" Property="Background" Value="Red" />
</DataTrigger>
<DataTrigger Binding="{Binding ConnectionState, Mode=OneWay}"
Value="{x:Static model:ConnectionStateType.AVAILABLE}">
<Setter TargetName="myContent" Property="Background"
Value="GREEN" />
</DataTrigger>
<DataTrigger Binding="{Binding ConnectionState, Mode=OneWay}"
Value="{x:Static model:ConnectionStateType.CONNECTED}">
<Setter TargetName="myContent" Property="Content">
<Setter.Value>
<Image Source="Images/User.png" />
</Setter.Value>
</Setter>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<!-- DEBUG Connection State -->
<DataGridTextColumn Header="DEBUG"
Binding="{Binding ConnectionState}" Width="SizeToCells" />
<!-- Sensor Name -->
<DataGridTextColumn Header="Sensor Name"
Binding="{Binding Name}" Width="SizeToCells" />
<!-- IPAddress -->
<DataGridTextColumn Header="IP Address"
Binding="{Binding IPAddress}" Width="SizeToCells" />
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
Here's the App.xaml.cs where I bootstrap everything and simulate sensors being discovered asynchronously (fyi, the same issue occurs if I load them serially, it's just easier to see when the sensors are loaded slowly one at a time:
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
public const int NUM_SENSORS = 100;
Random _connectionStateGenerator = new Random();
ConnectionStateType _connectionState = ConnectionStateType.AVAILABLE;
SensorViewModel viewModel;
Timer _timer = new Timer(300);
int _index = 1;
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// Get a handle to the main view (MainWindow)
Window window = new MainWindow();
viewModel = new SensorViewModel();
//Loads sensors synchronously (same issue)
//AddSensors(viewModel.Sensors);
window.DataContext = viewModel;
window.Show();
//Simulate async sensor discovery (more real world example)
_timer.Enabled = false;
_timer.AutoReset = true;
_timer.Elapsed += (s, args) =>
{
Dispatcher.InvokeAsync(new Action( () =>
{
viewModel.Sensors.Add(
new Sensor("Sensor" + _index, "192.168.1." + _index,
(ConnectionStateType)_connectionStateGenerator.Next(0, 3)));
if (_index++ > NUM_SENSORS)
_timer.Enabled = false;
}));
};
_timer.Enabled = true;
}
//Helper for loading synchronously rather than asynchronously
private void AddSensors(ObservableCollection<Model.Sensor> sensors)
{
for (int i = 0; i < NUM_SENSORS; i++)
{
_connectionState = (ConnectionStateType)_connectionStateGenerator
.Next(0, 5);
sensors.Add(
new Sensor("Sensor" + i, "192.168.1." + i, _connectionState));
}
}
}
Here's the model code representing a sensor:
public enum ConnectionStateType
{
NOT_FOUND,
AVAILABLE,
CONNECTED,
}
public class Sensor : INotifyPropertyChanged
{
string _name = "Unknown";
string _IPAddress;
ConnectionStateType _connectionState = ConnectionStateType.AVAILABLE;
public Sensor(string name, string IPAddress, ConnectionStateType connectionState)
{
_name = name;
_IPAddress = IPAddress;
_connectionState = connectionState;
}
public ConnectionStateType ConnectionState
{
get { return _connectionState; }
set
{
if (value == _connectionState) return;
_connectionState = value;
NotifyPropertyChanged("ConnectionState");
}
}
public string Name
{
get { return _name; }
set
{
if (value == _name) return;
_name = value;
NotifyPropertyChanged("Name");
}
}
public string IPAddress
{
get { return _IPAddress; }
set
{
if (value == _IPAddress) return;
_IPAddress = value;
NotifyPropertyChanged("IPAddress");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string property)
{
var handler = PropertyChanged;
if(handler != null)
handler(this, new PropertyChangedEventArgs(property));
}
}
Here's the View Model:
public class SensorViewModel : INotifyPropertyChanged
{
ObservableCollection<Sensor> _sensors = new ObservableCollection<Sensor>();
public ObservableCollection<Sensor> Sensors
{
get { return _sensors; }
private set
{
if (value == _sensors) return;
_sensors = value;
NotifyPropertyChanged("Sensors");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string property)
{
var handler = PropertyChanged;
if(handler != null)
handler(this, new PropertyChangedEventArgs(property));
}
}
Thanks for your help.
The solution ended up being to have the trigger swap out the entire template rather than trying to set the Content area in the template.
Here is a cleaner piece XAML with template resources defined and a DataGrid that uses those resources:
<UserControl x:Class="StackOverflowGridIssue.SensorGrid"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:model="clr-namespace:StackOverflowGridIssue.Model"
xmlns:shape="http://schemas.microsoft.com/expression/2010/drawing"
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"
mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<ControlTemplate x:Key="DefaultConnectionStateTemplate"
TargetType="{x:Type ContentControl}">
<Grid>
<Ellipse Height="10" Width="10"
Fill="{TemplateBinding Background}"
Stroke="{TemplateBinding Background}" />
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
</ControlTemplate>
<ControlTemplate x:Key="ConnectedTemplate"
TargetType="{x:Type ContentControl}" x:Shared="false">
<Grid>
<Image Source="Images/User.png" Height="25" Width="25"/>
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
</ControlTemplate>
<DataTemplate x:Key="SensorConnectionStateTemplate">
<ContentControl x:Name="myContent">
<ContentControl.ToolTip>
<TextBlock Text="{Binding ConnectionState, Mode=OneWay}" />
</ContentControl.ToolTip>
<ContentControl.Style>
<Style TargetType="{x:Type ContentControl}" BasedOn="{x:Null}" >
<Setter Property="Template"
Value="{StaticResource DefaultConnectionStateTemplate}" />
</Style>
</ContentControl.Style>
</ContentControl>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding ConnectionState, Mode=OneWay}"
Value="{x:Static model:ConnectionStateType.NOT_FOUND}">
<Setter TargetName="myContent" Property="Background" Value="Red" />
<Setter TargetName="myContent" Property="Content" Value="Not Found" />
</DataTrigger>
<DataTrigger Binding="{Binding ConnectionState, Mode=OneWay}"
Value="{x:Static model:ConnectionStateType.AVAILABLE}">
<Setter TargetName="myContent" Property="Background" Value="GREEN" />
<Setter TargetName="myContent" Property="Content" Value="Available" />
</DataTrigger>
<DataTrigger Binding="{Binding ConnectionState, Mode=OneWay}"
Value="{x:Static model:ConnectionStateType.CONNECTED}">
<Setter TargetName="myContent" Property="Template"
Value="{StaticResource ConnectedTemplate}" />
<Setter TargetName="myContent" Property="Content" Value="Connected" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</UserControl.Resources>
<Grid>
<DataGrid Name="_SensorsDataGrid" AutoGenerateColumns="False"
ItemsSource="{Binding Sensors}"
HeadersVisibility="Column" >
<DataGrid.Columns>
<!-- Status -->
<DataGridTemplateColumn Header="Status" MinWidth="50"
Width="SizeToHeader" IsReadOnly="True"
CellTemplate="{StaticResource SensorConnectionStateTemplate}" />
<!-- DEBUG Connection State -->
<DataGridTextColumn Header="DEBUG"
Binding="{Binding ConnectionState}" Width="SizeToCells" />
<!-- Sensor Name -->
<DataGridTextColumn Header="Sensor Name"
Binding="{Binding Name}" Width="SizeToCells" />
<!-- IPAddress -->
<DataGridTextColumn Header="IP Address" Width="SizeToCells"
Binding="{Binding IPAddress}" />
</DataGrid.Columns>
</DataGrid>
</Grid>
</UserControl>

Xaml Namescope and Template Question Concerning WPF ContextMenu

Everything in the code below works except for the binding on the ContextMenu. This is evidently due to the fact that the ContextMenu is located inside of a Style, which puts it in a different namescope from the rest of the xaml. I am looking for a solution where I won't have to instantiate a ContextMenu in the code-behind, since the application where I have to apply the solution contains a very large ContextMenu with a lot of bindings. There must be a way to accomplish this in xaml, otherwise it would seem like a serious oversight. Also note that I've already tried traversing the element tree using VisualTreeHelper and LogicalTreeHelper, but I wasn't able to find the ContextMenu from the root element of the Window (these classes evidently skipped over the interesting elements). Anyway, all of the code is below. This can be pasted into a new WPF application in Visual Studio, and nothing is missing.
Here's the code for App.xaml.cs (the xaml was left unchanged):
using System.Windows;
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
WindowV windowV = new WindowV();
WindowVM windowVM = new WindowVM();
windowV.DataContext = windowVM;
windowV.Show();
}
}
}
Here's the xaml for what was originally Window1:
<Window x:Class="WpfApplication1.WindowV"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:src="clr-namespace:WpfApplication1"
Name="MainWindow"
Title="WindowV" Height="300" Width="300">
<Window.Resources>
<Style TargetType="{x:Type ItemsControl}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsLocked}" Value="true">
<Setter Property="ItemsSource" Value="{Binding LockedList}" />
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
<TextBlock Text="{Binding}" />
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding IsLocked}" Value="false">
<Setter Property="ItemsSource" Value="{Binding RegularList}" />
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
<TextBlock Text="{Binding}">
<TextBlock.ContextMenu>
<ContextMenu>
<MenuItem Header="{Binding MenuItem1, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" />
<MenuItem Header="{Binding MenuItem2, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" />
<MenuItem Header="{Binding MenuItem3, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" />
</ContextMenu>
</TextBlock.ContextMenu>
</TextBlock>
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="4*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ItemsControl Grid.Row="0" />
<Button Name="ToggleButton"
Grid.Row="1"
Content="Toggle Lock"
Click="OnToggleLock" />
</Grid>
</Window>
Here's the codebehind for what was originally Window1:
using System.Windows;
using System.Windows.Markup;
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class WindowV : Window
{
public WindowV()
{
InitializeComponent();
}
private void OnToggleLock(object sender, RoutedEventArgs e)
{
if (((WindowVM)(DataContext)).IsLocked == true)
((WindowVM)(DataContext)).IsLocked = false;
else
((WindowVM)(DataContext)).IsLocked = true;
}
}
}
A new class was added to the project called WindowVM. Here's its code:
using System.Collections.Generic;
using System.ComponentModel;
namespace WpfApplication1
{
public class WindowVM : INotifyPropertyChanged
{
public string MenuItem1
{
get
{
string str = "Menu item 1";
return str;
}
}
public string MenuItem2
{
get
{
string str = "Menu item 2";
return str;
}
}
public string MenuItem3
{
get
{
string str = "Menu item 3";
return str;
}
}
public List<string> LockedList
{
get
{
List<string> list = new List<string>();
list.Add("This items control is currently locked.");
return list;
}
}
public List<string> RegularList
{
get
{
List<string> list = new List<string>();
list.Add("Item number 1.");
list.Add("Item number 2.");
list.Add("Item number 3.");
return list;
}
}
private bool _isLocked;
public bool IsLocked
{
get { return _isLocked; }
set
{
if (_isLocked != value)
{
_isLocked = value;
OnPropertyChanged("IsLocked");
}
}
}
public WindowVM()
{
IsLocked = false;
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string PropertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
}
}
}
Any insight would be very appreciated. Thanks much!
Andrew
Okay - I am having a little trouble following what you are trying to accomplish - but try the following:
<ContextMenu>
<MenuItem Header="{Binding DataContext.MenuItem1, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" />
...
And to make your code easier, you can then try:
<ContextMenu DataContext="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}">
<MenuItem Header="{Binding MenuItem1}"/>
...
Problem was that you were binding to the UI element Window which does not have a property called MenuItem1 etc. The DataContext property has the data you are interested in.
Alright, this solution works: I changed the WindowV.xaml and WindowV.xaml.cs files as follows. The following corrections fix the problem concerning namescope in xaml.
Here's the new WindowV.xaml file:
<Window x:Class="WpfApplication1.WindowV"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:src="clr-namespace:WpfApplication1"
Name="RootElement"
Title="WindowV" Height="300" Width="300">
<Window.Resources>
<ContextMenu x:Key="myContextMenu" DataContext="{Binding Path=DataContext, ElementName=RootElement}">
<MenuItem Header="{Binding MenuItem1}" />
<MenuItem Header="{Binding MenuItem2}" />
<MenuItem Header="{Binding MenuItem3}" />
</ContextMenu>
<Style TargetType="{x:Type ItemsControl}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsLocked}" Value="true">
<Setter Property="ItemsSource" Value="{Binding LockedList}" />
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
<TextBlock Text="{Binding}" />
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding IsLocked}" Value="false">
<Setter Property="ItemsSource" Value="{Binding RegularList}" />
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
<TextBlock Text="{Binding}" ContextMenu="{StaticResource myContextMenu}" />
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="4*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ItemsControl Grid.Row="0" />
<Button Name="ToggleButton"
Grid.Row="1"
Content="Toggle Lock"
Click="OnToggleLock" />
</Grid>
</Window>
Here's the corresponding code-behind:
using System.Windows;
using System.Windows.Markup;
using System;
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class WindowV : Window
{
public WindowV()
{
InitializeComponent();
System.Windows.Controls.ContextMenu contextMenu =
FindResource("myContextMenu") as System.Windows.Controls.ContextMenu;
NameScope.SetNameScope(contextMenu, NameScope.GetNameScope(this as DependencyObject));
contextMenu.RegisterName("RootElement", this);
}
private void OnToggleLock(object sender, RoutedEventArgs e)
{
if (((WindowVM)(DataContext)).IsLocked == true)
((WindowVM)(DataContext)).IsLocked = false;
else
((WindowVM)(DataContext)).IsLocked = true;
}
}
}
Andrew

Set ListBoxItem.IsSelected when child TextBox is Focused

I have a typical MVVM scenario:
I have a ListBox that is binded to a List of StepsViewModels.
I define a DataTemplate so that StepViewModels are rendered as StepViews.
The StepView UserControl have a set of labels and TextBoxs.
What I want to do is to select the ListBoxItem that is wrapping the StepView when a textBox is focused. I've tried to create a style for my TextBoxs with the following trigger:
<Trigger Property="IsFocused" Value="true">
<Setter TargetName="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}}}" Property="IsSelected" Value="True"/>
</Trigger>
But I get an error telling me that TextBoxs don't have an IsSelected property. I now that but the Target is a ListBoxItem.
How can I make it work?
There is a read-only property IsKeyboardFocusWithin that will be set to true if any child is focused. You can use this to set ListBoxItem.IsSelected in a Trigger:
<ListBox ItemsSource="{Binding SomeCollection}" HorizontalAlignment="Left">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Style.Triggers>
<Trigger Property="IsKeyboardFocusWithin" Value="True">
<Setter Property="IsSelected" Value="True" />
</Trigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBox Width="100" Margin="5" Text="{Binding Name}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
As Jordan0Day correctly pointed out there can be indeed big problems using IsKeyboardFocusWithin solution. In my case a Button in a Toolbar which regards to the ListBox was also not working anymore. The same problem with focus. When clicking the button the ListBoxItem does loose the Focus and the Button updated its CanExecute method, which resulted in disabling the button just a moment before the button click command should be executed.
For me a much better solution was to use a ItemContainerStyle EventSetter as described in this post: ListboxItem selection when the controls inside are used
XAML:
<Style x:Key="MyItemContainer.Style" TargetType="{x:Type ListBoxItem}">
<Setter Property="Background" Value="LightGray"/>
<EventSetter Event="GotKeyboardFocus" Handler="OnListBoxItemContainerFocused" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Border x:Name="backgroundBorder" Background="White">
<ContentPresenter Content="{TemplateBinding Content}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="backgroundBorder" Property="Background" Value="#FFD7E6FC"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
EventHandler in the code behind of the view:
private void OnListBoxItemContainerFocused(object sender, RoutedEventArgs e)
{
(sender as ListBoxItem).IsSelected = true;
}
One way to achieve that is by implementing a custom behavior using an attached property. Basically, the attached property would be applied to the ListBoxItem using a style, and would hook up to their GotFocus event. That even fires if any descendant of the control gets the focus, so it is suitable for this task. In the event handler, IsSelected is set to true.
I wrote up a small example for you:
The Behavior Class:
public class MyBehavior
{
public static bool GetSelectOnDescendantFocus(DependencyObject obj)
{
return (bool)obj.GetValue(SelectOnDescendantFocusProperty);
}
public static void SetSelectOnDescendantFocus(
DependencyObject obj, bool value)
{
obj.SetValue(SelectOnDescendantFocusProperty, value);
}
public static readonly DependencyProperty SelectOnDescendantFocusProperty =
DependencyProperty.RegisterAttached(
"SelectOnDescendantFocus",
typeof(bool),
typeof(MyBehavior),
new UIPropertyMetadata(false, OnSelectOnDescendantFocusChanged));
static void OnSelectOnDescendantFocusChanged(
DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ListBoxItem lbi = d as ListBoxItem;
if (lbi == null) return;
bool ov = (bool)e.OldValue;
bool nv = (bool)e.NewValue;
if (ov == nv) return;
if (nv)
{
lbi.GotFocus += lbi_GotFocus;
}
else
{
lbi.GotFocus -= lbi_GotFocus;
}
}
static void lbi_GotFocus(object sender, RoutedEventArgs e)
{
ListBoxItem lbi = sender as ListBoxItem;
lbi.IsSelected = true;
}
}
The Window XAML:
<Window x:Class="q2960098.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
Title="MainWindow" Height="350" Width="525" xmlns:my="clr-namespace:q2960098">
<Window.Resources>
<DataTemplate x:Key="UserControlItemTemplate">
<Border BorderBrush="Black" BorderThickness="5" Margin="10">
<my:UserControl1/>
</Border>
</DataTemplate>
<XmlDataProvider x:Key="data">
<x:XData>
<test xmlns="">
<item a1="1" a2="2" a3="3" a4="4">a</item>
<item a1="a" a2="b" a3="c" a4="d">b</item>
<item a1="A" a2="B" a3="C" a4="D">c</item>
</test>
</x:XData>
</XmlDataProvider>
<Style x:Key="MyBehaviorStyle" TargetType="ListBoxItem">
<Setter Property="my:MyBehavior.SelectOnDescendantFocus" Value="True"/>
</Style>
</Window.Resources>
<Grid>
<ListBox ItemTemplate="{StaticResource UserControlItemTemplate}"
ItemsSource="{Binding Source={StaticResource data}, XPath=//item}"
HorizontalContentAlignment="Stretch"
ItemContainerStyle="{StaticResource MyBehaviorStyle}">
</ListBox>
</Grid>
</Window>
The User Control XAML:
<UserControl x:Class="q2960098.UserControl1"
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"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UniformGrid>
<TextBox Margin="10" Text="{Binding XPath=#a1}"/>
<TextBox Margin="10" Text="{Binding XPath=#a2}"/>
<TextBox Margin="10" Text="{Binding XPath=#a3}"/>
<TextBox Margin="10" Text="{Binding XPath=#a4}"/>
</UniformGrid>
</UserControl>
If you create a User Control and then use it as the DataTemplate It seems to work cleaner.
Then you don't have to use the dirty Style Triggers that Don't work 100% of the time.
Edit: Someone else already had the same answer on a different question: https://stackoverflow.com/a/7555852/2484737
Continuing on Maexs' answer, using an EventTrigger instead of an EventSetter removes the need for code-behind:
<Style.Triggers>
<EventTrigger RoutedEvent="GotKeyboardFocus">
<BeginStoryboard>
<Storyboard >
<BooleanAnimationUsingKeyFrames Storyboard.TargetProperty="IsSelected" >
<DiscreteBooleanKeyFrame Value="True" KeyTime="0:0:0"/>
</BooleanAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Style.Triggers>

How to declare combobox itemTemplate that has Itemsource as Enum Values in WPF?

I have a enum let's say
enum MyEnum
{
FirstImage,
SecondImage,
ThirdImage,
FourthImage
};
I have binded this Enum to my combobox in XAML.
While defining an combobox I have defined an ItemTemplate of combox to take Two UI element:
TextBlock that show the enum value (Description)
Image
I have done this much in XAML.
I am wondering where I can specify the Image corrosponding to each item of Enum value in a combobox? Is that possible through data trigger ?
I really appreciate if anyone have the XAML for this scenario.
Many Thanks in advance
You can use a DataTrigger, but would be more maintainable if you used a Converter. Here's a sample that uses a DataTrigger for a view of the image and text by itself, and then the same DataTrigger to display the image and text in ListBox and ComboBox, and finally, a ListBox and ComboBox that use a Converter to display the image and text:
XAML
<Window x:Class="WpfSandbox.EnumToImage.EnumToImage"
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:local="clr-namespace:WpfSandbox.EnumToImage"
Title="Enum To Image" SizeToContent="WidthAndHeight" >
<Window.DataContext>
<local:ImageViewModel x:Name="Model" />
</Window.DataContext>
<Window.Resources>
<ObjectDataProvider x:Key="EnumDataProvider"
MethodName="GetValues"
ObjectType="{x:Type System:Enum}">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="local:Decade"/>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
<local:DecadeEnumImageConverter x:Key="ImageConverter" />
<ControlTemplate x:Key="ImageTemplate" >
<StackPanel Orientation="Horizontal">
<Image x:Name="MyImage" Width="64" Height="32" />
<TextBlock Text="{Binding}" VerticalAlignment="Center" />
</StackPanel>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding}" Value="Ninties" >
<DataTrigger.Setters>
<Setter TargetName="MyImage"
Property="Source"
Value="/EnumToImage/images/90s.jpg"/>
</DataTrigger.Setters>
</DataTrigger>
<DataTrigger Binding="{Binding}" Value="Eighties" >
<DataTrigger.Setters>
<Setter TargetName="MyImage"
Property="Source"
Value="/EnumToImage/images/80s.jpg"/>
</DataTrigger.Setters>
</DataTrigger>
<DataTrigger Binding="{Binding}" Value="Seventies" >
<DataTrigger.Setters>
<Setter TargetName="MyImage"
Property="Source"
Value="/EnumToImage/images/70s.jpg"/>
</DataTrigger.Setters>
</DataTrigger>
<DataTrigger Binding="{Binding}" Value="Sixties" >
<DataTrigger.Setters>
<Setter TargetName="MyImage"
Property="Source"
Value="/EnumToImage/images/60s.jpg"/>
</DataTrigger.Setters>
</DataTrigger>
<DataTrigger Binding="{Binding}" Value="Fifties" >
<DataTrigger.Setters>
<Setter TargetName="MyImage"
Property="Source"
Value="/EnumToImage/images/50s.jpg"/>
</DataTrigger.Setters>
</DataTrigger>
<DataTrigger Binding="{Binding}" Value="Forties" >
<DataTrigger.Setters>
<Setter TargetName="MyImage"
Property="Source"
Value="/EnumToImage/images/40s.jpg"/>
</DataTrigger.Setters>
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<DataTemplate x:Key="ItemsTemplateWithConverter">
<StackPanel Orientation="Horizontal">
<Image Width="64" Height="32"
Source="{Binding Converter={StaticResource ImageConverter}}"/>
<TextBlock Text="{Binding}" VerticalAlignment="Center" />
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="ItemsTemplateWithDataTrigger">
<ContentControl Template="{StaticResource ImageTemplate}" />
</DataTemplate>
</Window.Resources>
<StackPanel>
<ContentControl Margin="10" MouseUp="OnImageMouseUp"
HorizontalAlignment="Center" Cursor="Hand"
DataContext="{Binding Path=ImageEnum}"
Template="{StaticResource ImageTemplate}" />
<StackPanel Orientation="Horizontal">
<StackPanel>
<ListView Margin="10"
ItemsSource="{Binding Source={StaticResource EnumDataProvider}}"
ItemTemplate="{StaticResource ItemsTemplateWithConverter}" />
<ComboBox Margin="10"
ItemsSource="{Binding Source={StaticResource EnumDataProvider}}"
ItemTemplate="{StaticResource ItemsTemplateWithConverter}" />
</StackPanel>
<StackPanel>
<ListView Margin="10"
ItemsSource="{Binding Source={StaticResource EnumDataProvider}}"
ItemTemplate="{StaticResource ItemsTemplateWithDataTrigger}" />
<ComboBox Margin="10"
ItemsSource="{Binding Source={StaticResource EnumDataProvider}}"
ItemTemplate="{StaticResource ItemsTemplateWithDataTrigger}" />
</StackPanel>
</StackPanel>
</StackPanel>
</Window>
Code Behind
using System;
using System.ComponentModel;
using System.Globalization;
using System.Windows;
using System.Windows.Input;
using System.Windows.Data;
namespace WpfSandbox.EnumToImage
{
/// <summary>
/// Interaction logic for EnumToImage.xaml
/// </summary>
public partial class EnumToImage : Window
{
public EnumToImage()
{
InitializeComponent();
}
private int i = 1;
private void OnImageMouseUp( object sender, MouseButtonEventArgs e )
{
i++;
Model.ImageEnum = ( Decade )i;
if( i == 6 )
i = 0;
}
}
public enum Decade
{
Ninties = 1,
Eighties = 2,
Seventies = 3,
Sixties = 4,
Fifties = 5,
Forties = 6,
};
public class ImageViewModel : INotifyPropertyChanged
{
private Decade _imageEnum;
public Decade ImageEnum
{
get { return _imageEnum; }
set
{
_imageEnum = value;
RaisePropertyChanged( "ImageEnum" );
}
}
public ImageViewModel()
{
ImageEnum = Decade.Ninties;
}
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged( string propertyName )
{
var handler = PropertyChanged;
if( handler != null )
{
handler( this, new PropertyChangedEventArgs( propertyName ) );
}
}
}
public class DecadeEnumImageConverter : IValueConverter
{
public object Convert( object value, Type targetType, object parameter, CultureInfo culture )
{
var myEnum = ( Decade )Enum.Parse( typeof( Decade ), value.ToString() );
switch( myEnum )
{
case Decade.Ninties:
return "/EnumToImage/images/90s.jpg";
case Decade.Eighties:
return "/EnumToImage/images/80s.jpg";
case Decade.Seventies:
return "/EnumToImage/images/70s.jpg";
case Decade.Sixties:
return "/EnumToImage/images/60s.jpg";
case Decade.Fifties:
return "/EnumToImage/images/50s.jpg";
case Decade.Forties:
return "/EnumToImage/images/40s.jpg";
default:
throw new ArgumentOutOfRangeException();
}
}
public object ConvertBack( object value, Type targetType, object parameter, CultureInfo culture )
{
throw new NotImplementedException();
}
}
}

Resources