WPF: Keeping track of relative item positions in ItemsControl/ListBox - wpf

Please see the following code.
It creates a ListBox with five items. The selected item of the ListBox is colored in yellow, previous items (index below selected index) are colored in green and future items (index above selected index) are colored in red.
ItemViewModel.vb
Public Class ItemViewModel
Implements INotifyPropertyChanged
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Private _title As String
Private _isOld As Boolean
Private _isNew As Boolean
Protected Overridable Sub OnPropertyChanged(<CallerMemberName> Optional propertyName As String = Nothing)
If String.IsNullOrEmpty(propertyName) Then
Exit Sub
End If
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
Public Property Title As String
Get
Return _title
End Get
Set(value As String)
_title = value
Me.OnPropertyChanged()
End Set
End Property
Public Property IsOld As Boolean
Get
Return _isOld
End Get
Set(value As Boolean)
_isOld = value
Me.OnPropertyChanged()
End Set
End Property
Public Property IsNew As Boolean
Get
Return _isNew
End Get
Set(value As Boolean)
_isNew = value
Me.OnPropertyChanged()
End Set
End Property
End Class
MainViewModel:
Public Class MainViewModel
Implements INotifyPropertyChanged
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Private ReadOnly _items As ObservableCollection(Of ItemViewModel)
Private _selectedIndex As Integer
Public Sub New()
_items = New ObservableCollection(Of ItemViewModel)
_items.Add(New ItemViewModel With {.Title = "Very old"})
_items.Add(New ItemViewModel With {.Title = "Old"})
_items.Add(New ItemViewModel With {.Title = "Current"})
_items.Add(New ItemViewModel With {.Title = "New"})
_items.Add(New ItemViewModel With {.Title = "Very new"})
Me.SelectedIndex = 0
End Sub
Protected Overridable Sub OnPropertyChanged(<CallerMemberName> Optional propertyName As String = Nothing)
If String.IsNullOrEmpty(propertyName) Then
Exit Sub
End If
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
Public ReadOnly Property Items As ObservableCollection(Of ItemViewModel)
Get
Return _items
End Get
End Property
Public Property SelectedIndex As Integer
Get
Return _selectedIndex
End Get
Set(value As Integer)
_selectedIndex = value
Me.OnPropertyChanged()
For index As Integer = 0 To Me.Items.Count - 1
Me.Items(index).IsOld = (index < Me.SelectedIndex)
Me.Items(index).IsNew = (index > Me.SelectedIndex)
Next index
End Set
End Property
End Class
MainWindow.xaml
<Window x:Class="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:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="300" Width="200">
<Window.DataContext>
<local:MainViewModel />
</Window.DataContext>
<ListBox ItemsSource="{Binding Items}" SelectedIndex="{Binding SelectedIndex}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Title}">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsOld}" Value="True">
<Setter Property="Foreground" Value="Green" />
</DataTrigger>
<DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBoxItem}}}" Value="True">
<Setter Property="Foreground" Value="Yellow" />
</DataTrigger>
<DataTrigger Binding="{Binding IsNew}" Value="True">
<Setter Property="Foreground" Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Window>
This works like expected, but I don't like, that the ItemViewModel holds the properties IsOld and IsNew and that the MainViewModel is responsible for updating these properties. In my opinion that should be done by the ListBox, not by every view model that might be the DataContext for my ListBox.
I already tried to create two attached properties for ListBoxItem and bind to them (like I bound to IsSelected for the current item). But I couldn't figure out an event on which I update those attached properties.
Is using these attached properties the way to go? When and/or where do I update those attached properties?
I tried to attach to the ValueChanged event of the ItemsSource property of the ListBox to be able to attach to the CollectionChanged event of the underlying collection. But I failed getting the ListBoxItem for an item, since these containers are created asynchronously (so I assume). And since the ListBox uses a VirtualizingStackPanel by default, I wouldn't get a ListBoxItem for every item of my underlying collection anyway.
Please keep in mind that the collection of items I bind to is observable and can change. So the IsOld and IsNew properties have to be updated whenever the source collection itself changes, whenever the content of the source collection changes and whenever the selected index changes.
Or how else can I achieve what I like to achieve?
I didn't flag VB.net on purpose since the question doesn't have anything to do with VB.net and I'm fine with answers in C# as well.
Thank you.

One way you can achieve this is through an attached behavior. This allows you to keep the display behavior with the ListBox and away from your view-model, etc.
First, I created an enum to store the states of the items:
namespace WpfApp4
{
public enum ListBoxItemAge
{
Old,
Current,
New,
None
}
}
Next, I created an attached behavior class with two attached properties:
IsActive (bool) = Turns on the behavior for the ListBox
ItemAge (ListBoxItemAge) = Determines if an item should be displayed in Red, Yellow, Green, etc.
When IsActive is set to True on a ListBox, it will subscribe to the SelectionChanged event and will handle setting each ListBoxItems age.
Here is the code:
using System.Windows;
using System.Windows.Controls;
namespace WpfApp4
{
public class ListBoxItemAgeBehavior
{
#region IsActive (Attached Property)
public static readonly DependencyProperty IsActiveProperty =
DependencyProperty.RegisterAttached(
"IsActive",
typeof(bool),
typeof(ListBoxItemAgeBehavior),
new PropertyMetadata(false, OnIsActiveChanged));
public static bool GetIsActive(DependencyObject obj)
{
return (bool)obj.GetValue(IsActiveProperty);
}
public static void SetIsActive(DependencyObject obj, bool value)
{
obj.SetValue(IsActiveProperty, value);
}
private static void OnIsActiveChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is ListBox listBox)) return;
if ((bool) e.NewValue)
{
listBox.SelectionChanged += OnSelectionChanged;
}
else
{
listBox.SelectionChanged -= OnSelectionChanged;
}
}
private static void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var listBox = (ListBox) sender;
var selectedIndex = listBox.SelectedIndex;
SetItemAge(listBox.ItemContainerGenerator.ContainerFromIndex(selectedIndex), ListBoxItemAge.Current);
foreach (var item in listBox.ItemsSource)
{
var index = listBox.Items.IndexOf(item);
if (index < selectedIndex)
{
SetItemAge(listBox.ItemContainerGenerator.ContainerFromIndex(index), ListBoxItemAge.Old);
}
else if (index > selectedIndex)
{
SetItemAge(listBox.ItemContainerGenerator.ContainerFromIndex(index), ListBoxItemAge.New);
}
}
}
#endregion
#region ItemAge (Attached Property)
public static readonly DependencyProperty ItemAgeProperty =
DependencyProperty.RegisterAttached(
"ItemAge",
typeof(ListBoxItemAge),
typeof(ListBoxItemAgeBehavior),
new FrameworkPropertyMetadata(ListBoxItemAge.None));
public static ListBoxItemAge GetItemAge(DependencyObject obj)
{
return (ListBoxItemAge)obj.GetValue(ItemAgeProperty);
}
public static void SetItemAge(DependencyObject obj, ListBoxItemAge value)
{
obj.SetValue(ItemAgeProperty, value);
}
#endregion
}
}
The XAML looks something like this. This is just a simple example:
<ListBox
local:ListBoxItemAgeBehavior.IsActive="True"
ItemsSource="{Binding Data}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Title}">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=(local:ListBoxItemAgeBehavior.ItemAge), RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}" Value="Old">
<Setter Property="Foreground" Value="Red" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=(local:ListBoxItemAgeBehavior.ItemAge), RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}" Value="Current">
<Setter Property="Foreground" Value="Yellow" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=(local:ListBoxItemAgeBehavior.ItemAge), RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}" Value="New">
<Setter Property="Foreground" Value="Green" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I've created three DataTriggers that look for the value of the ListBoxItemAgeBehavior.ItemAge and then set the appropriate Foreground color. Since the attached property is set on the ListBoxItem, I'm doing a RelativeSource on the binding.
I hope this helps.

Related

Bind WPF Combobox Item Background

I'm trying to have a list from a combobox that highlights certain values, and the criteria for highlighting is a boolean. I have been able to make it work in testing by manually adding each combobox item manually and marking the tag, but I need it to be bound to be more dynamic. I've tried a couple different ways, but the dictionary seems like the simplest.
Dictionary
XAML:
<ComboBox Name="Box" HorizontalAlignment="Left" Margin="81,102,0,0" VerticalAlignment="Top" Width="120" ItemsSource="{Binding Items.Keys}">
<ComboBox.Resources>
<Style TargetType="{x:Type ComboBoxItem}">
<Style.Triggers>
<DataTrigger Binding="{Binding Items.Values}" Value="True">
<Setter Property="Background" Value="Yellow"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.Resources>
</ComboBox>
Code-behind:
Public Module GlobalVariables
Public Item As New TestItem
End Module
Class MainWindow
Public Sub New()
InitializeComponent()
DataContext = Item
End Sub
End Class
Public Class TestItem
Public Property Items As New Dictionary(Of String, Boolean)
Public Sub New()
Items.Add("1", False)
Items.Add("2", True)
Items.Add("3", False)
Items.Add("4", False)
Items.Add("5", True)
End Sub
End Class
I'm guessing that my issue is that I'm using the collection of the dictionary values as the datatrigger binding rather than the individual one, but I'm not sure how to get the value associated with the key in XAML.
Thanks for any help
EDIT
Thanks to Tarazed's answer, I was able to get the test to work with this:
XAML:
<ComboBox Name="Box" HorizontalAlignment="Left" Margin="81,102,0,0" VerticalAlignment="Top" Width="120" ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedObject}" DisplayMemberPath="Name">
<ComboBox.Resources>
<Style TargetType="{x:Type ComboBoxItem}">
<Style.Triggers>
<DataTrigger Binding="{Binding Highlighted}" Value="True">
<Setter Property="Background" Value="Yellow"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.Resources>
</ComboBox>
Code-behind:
Public Module GlobalVariables
Public TestCollection As New ItemCollection
End Module
Class MainWindow
Public Sub New()
InitializeComponent()
DataContext = TestCollection
End Sub
End Class
Public Class Item
Public Property Highlighted As Boolean
Public Property Name As String
End Class
Public Class ItemCollection
Public Property Items As New List(Of Item)
Public Property SelectedObject As Item
Public Sub New()
Items.Add(New Item With {.Name = "1", .Highlighted = False})
Items.Add(New Item With {.Name = "2", .Highlighted = True})
Items.Add(New Item With {.Name = "3", .Highlighted = False})
Items.Add(New Item With {.Name = "4", .Highlighted = False})
Items.Add(New Item With {.Name = "5", .Highlighted = True})
End Sub
End Class
You are correct, the binding is unable to associate the boolean value with the string because calling on .Keys and .Values produces two collectins.
Instead, take a look at the DisplayMemberPath of the ItemsControl (ComboBox). This allows you to pick the property of a complex object to be displayed by the control. Using this you can create a collection, rather than a dictionary, of instances of a custom structure or class containing the name and highlight boolean and choose the name for display.

WPF validation error style in reuseable UserControl?

I have made a reusable UserControl in my WPF application that has a Textbox (and in the real project other components) inside of it. I'm using Fluent Validation with INotifyDataErrorInfo to validate user input in TextBoxes. My problem is that when the model whose property is bound to the UserControl's TextBox has errors, the TextBox's style doesn't trigger according to the style that is set. It seems that my style's trigger for the UserControl's Textbox can't read the Validation.HasError value from the model correctly. So is there a way to get the style to trigger and get the error tooltip for the UserControl's Textbox?
This question has been asked by several other people over the years, and I have looked at every single one of them, but none of them really work for me. One thing I tried that does work is a general ValidationRule in the UserControl.xaml for the textbox binding, but that doesn't allow for model specific rules. I'm hoping that some WPF guru will finally take the challenge and solve this problem! :)
If you make a sample project from the code I provided, and set the Height property to less than 10, you see that the normal TextBox gets the triggered errorstyle with the tooltip, but the UserControl's TextBox gets the basic red border:
Sample app with the cursor over the first textbox.
Here is my simplified code:
The UserControl:
<UserControl x:Class="UserControlValidationTest.DataInputUserControl"
x:Name="parentControl"
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="450" d:DesignWidth="800">
<UserControl.Resources>
<Style TargetType="TextBox" x:Key="TextBoxStyle">
<Style.Triggers>
<Trigger Property= "Validation.HasError" Value="true">
<Setter Property="Background" Value="Pink"/>
<Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}" />
</Trigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
<StackPanel Orientation="Horizontal" DataContext="{Binding ElementName=parentControl}">
<TextBox Name="ValueBox" Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}" Width="60" Style="{StaticResource TextBoxStyle}"/>
</StackPanel>
public partial class DataInputUserControl : UserControl
{
public DataInputUserControl()
{
InitializeComponent();
Validation.SetValidationAdornerSite(this, ValueBox);
}
public double Value
{
get => (double)GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(double), typeof(DataInputUserControl), new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
}
MainWindow.xaml:
<Window x:Class="UserControlValidationTest.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:UserControlValidationTest"
mc:Ignorable="d"
Title="MainWindow" Height="100" Width="200">
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Window.Resources>
<Style TargetType="TextBox">
<Style.Triggers>
<Trigger Property= "Validation.HasError" Value="true">
<Setter Property="Background" Value="Pink"/>
<Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}" />
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<StackPanel>
<TextBox Text="{Binding User.Height, UpdateSourceTrigger=PropertyChanged}" Width="60" Margin="10"/>
<local:DataInputUserControl Value="{Binding User.Height}" HorizontalAlignment="Center"/>
</StackPanel>
View model:
public class MainWindowViewModel
{
public UserModel User { get; set; }
public MainWindowViewModel()
{
User = new UserModel();
}
}
User Model:
public class UserModel : ValidatableModel
{
public double Height { get => GetPropertyValue<double>(); set => SetPropertyValue(value); }
public UserModel()
{
ModelValidator = new UserValidator();
Height = 180;
}
}
User Validator:
public class UserValidator : AbstractValidator<UserModel>
{
public UserValidator()
{
RuleFor(x => x.Height)
.GreaterThan(10);
}
}
Validatable Model:
using FluentValidation;
using FluentValidation.Results;
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
public abstract class ValidatableModel : INotifyDataErrorInfo, INotifyPropertyChanged
{
private readonly Dictionary<string, object> propertyBackingDictionary = new Dictionary<string, object>();
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string parameter = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(parameter));
}
protected T GetPropertyValue<T>([CallerMemberName] string propertyName = null)
{
if (propertyBackingDictionary.TryGetValue(propertyName, out object value))
{
return (T)value;
}
return default(T);
}
protected bool SetPropertyValue<T>(T newValue, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(newValue, GetPropertyValue<T>(propertyName)))
{
return false;
}
propertyBackingDictionary[propertyName] = newValue;
OnPropertyChanged(propertyName);
Validate();
return true;
}
private ConcurrentDictionary<string, List<string>> errors = new ConcurrentDictionary<string, List<string>>();
public IValidator ModelValidator { get; set; }
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public void OnErrorsChanged(string propertyName)
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
}
public IEnumerable GetErrors([CallerMemberName] string propertyName = null)
{
errors.TryGetValue(propertyName, out List<string> errorsForName);
return errorsForName;
}
public bool HasErrors => errors.Count > 0;
public void Validate()
{
errors.Clear();
var validationResults = ModelValidator.Validate(this);
foreach (var item in validationResults.Errors)
{
errors.TryAdd(item.PropertyName, new List<string> { item.ErrorMessage });
OnErrorsChanged(item.PropertyName);
}
}
}
}

Make Button Visible Based on ComboBox selection

How do I make a Button show if a certain value is selected in a ComboBox using XAML ?
This is what I have tried.
Thanks
<ComboBox x:Name="ComboBox" Margin="171,102,426,271">
<ComboBoxItem>Testing</ComboBoxItem>
<ComboBoxItem>Again</ComboBoxItem>
<ComboBoxItem>Finally</ComboBoxItem>
</ComboBox>
<Button Margin="10, 0, 0, 0" >
<Button.Style>
<Style TargetType="Button">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedIndex, ElementName=ComboBox}" Value="Testing">
<Setter Property="Visibility" Value="Hidden"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
A better approach is to bind the controls to a view model and to integrate the logic there. See: Explain Combo Box Binding In MVVM - WPF.
As an example we create a window for editing of person data. It contains a combobox where the user can select a city. When a certain city is selected, a button is is displayed, otherwise it is hidden.
You could have a view model looking like this
public class PersonViewModel: INotifyPropertyChanged
{
private string _city;
public string City
{
get { return _city; }
set {
if (value != _city) {
_city = value;
OnPropertyChanged(nameof(City));
OnPropertyChanged(nameof(MyButtonVisibility));
}
}
}
public List<string> Cities { get; } = new List<string> { "Austin", "Boston", "Chicago"};
public Visibility MyButtonVisibility => City == "Boston"
? Visibility.Visible
: Visibility.Hidden;
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
// Other properties
private string _firstName;
public string FirstName
{
get { return _firstName; }
set {
if (value != _firstName) {
_firstName = value;
OnPropertyChanged(nameof(FirstName));
}
}
}
private string _lastName;
public string LastName
{
get { return _lastName; }
set {
if (value != _lastName) {
_lastName = value;
OnPropertyChanged(nameof(LastName));
}
}
}
}
Note that it implements INotifyPropertyChanged. It has a Cities collection used to display the combobox items and a City property for the selected city.
We also need a property for the button visibility (MyButtonVisibility). Note that when the selected city changes, we also raise the PropertyChanged event for MyButtonVisibility to tell WPF to requery the button visibility.
In the window's constructor we assign the view model:
public MainWindow()
{
InitializeComponent();
DataContext = new PersonViewModel();
}
The XAML code for the combobox is
<ComboBox x:Name="citiesComboBox" HorizontalAlignment="Left" Margin="116,96,0,0"
VerticalAlignment="Top" Width="120"
ItemsSource="{Binding Path=Cities}"
SelectedItem="{Binding Path=City}"
/>
The XAML code for the button is
<Button Content="Button" HorizontalAlignment="Left" Margin="116,164,0,0"
VerticalAlignment="Top" Width="75"
Visibility="{Binding MyButtonVisibility}"
/>
By the magic of WPF binding, now the button appears or disappears automatically, when you select cities.
The binding path should be SelectedItem.Content for your trigger to work:
<Button Margin="10, 0, 0, 0" >
<Button.Style>
<Style TargetType="Button">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedItem.Content, ElementName=ComboBox}" Value="Testing">
<Setter Property="Visibility" Value="Hidden"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
You are currently binding to the SelectedIndex property and this one never has a value of "Testing". The currently selected ComboBoxItem's Content property may have though.
If you want to show the Button when the "Testing" option is selected, you should also modify the value fo your setter:
<Button Margin="10, 0, 0, 0" >
<Button.Style>
<Style TargetType="Button">
<Setter Property="Visibility" Value="Hidden"/>
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedItem.Content, ElementName=ComboBox}" Value="Testing">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>

Access ComboBoxItem DisplayValue

I have a comboBox that has an ItemsSource of a list of objects. So the DisplayMemberPath is set to a particular property of the object. Of course this means that the correct value is displayed in the ComboBoxItem.
My issue is that I would like to be able to get that "Value" that is returned by the DisplayMemberPath in XAML so that I can bind it to something else. i.e. I would like to have a "DisplayText" property on the ComboBoxItem.
Of course I don't have this, so, does anyone know of a way to get this value without traversing down into the template of the ComboBoxItem looking for the ContentHost?
If you're interested in my specific use of this, I'm trying to do this on the style of the ComboBox:
....
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style>
<Setter
Property="AutomationProperties.AutomationId"
Value="{Binding RelativeSource={RelativeSource Self}, Path=MagicPathForDisplayedText}"/>
....
Of course Path=Content works just fine if you're just binding your ItemsSource to properties, but when it's an Object with a DisplayMemberPath, Content will be that Object.
Thanks for any help or re-framing of the problem.
The easiest way to handle problems like this is usually Attached Properties and Behaviors.
You could create two attached properties called DisplayMemberPath and DisplayText, then you bind DisplayMemberPath to the parent ComboBox DisplayMemberPath and in the PropertyChangedCallback you set up a binding of your own with the same path for DisplayText. After that you have a property which you can bind to
<Style x:Key="ComboBoxStyle" TargetType="ComboBox">
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="ComboBoxItem">
<Setter Property="behaviors:DisplayTextBehavior.DisplayMemberPath"
Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type ComboBox}},
Path=DisplayMemberPath}"/>
<Setter Property="AutomationProperties.AutomationId"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(behaviors:DisplayTextBehavior.DisplayText)}"/>
</Style>
</Setter.Value>
</Setter>
</Style>
DisplayTextBehavior
public class DisplayTextBehavior
{
public static DependencyProperty DisplayMemberPathProperty =
DependencyProperty.RegisterAttached("DisplayMemberPath",
typeof(string),
typeof(DisplayTextBehavior),
new FrameworkPropertyMetadata("", DisplayMemberPathChanged));
public static string GetDisplayMemberPath(DependencyObject obj)
{
return (string)obj.GetValue(DisplayMemberPathProperty);
}
public static void SetDisplayMemberPath(DependencyObject obj, string value)
{
obj.SetValue(DisplayMemberPathProperty, value);
}
private static void DisplayMemberPathChanged(object sender, DependencyPropertyChangedEventArgs e)
{
ComboBoxItem comboBoxItem = sender as ComboBoxItem;
string displayMemberPath = GetDisplayMemberPath(comboBoxItem);
comboBoxItem.SetBinding(DisplayTextProperty, new Binding(displayMemberPath));
}
public static DependencyProperty DisplayTextProperty =
DependencyProperty.RegisterAttached("DisplayText",
typeof(string),
typeof(DisplayTextBehavior),
new FrameworkPropertyMetadata(""));
public static string GetDisplayText(DependencyObject obj)
{
return (string)obj.GetValue(DisplayTextProperty);
}
public static void SetDisplayText(DependencyObject obj, string value)
{
obj.SetValue(DisplayTextProperty, value);
}
}
You could bind an intermediate Object in the ViewModel to the SelectedItem property on the Combobox. Then also Bind your other display item to that intermediate object. Then when the PropertyChanged event is fired by selecting an item, the display will also update via the event chain.
Are you using SelectedValuePath? If not you could could set it the same as DisplayMemberPath and then the selected value is available as SelectedValue.

Making the style's binding more flexible

I'm trying to make the existing style more flexible.
I'm attaching a dep. property to the double click event of the data grid row control.
On double-clicking the row i want a certain window to open.
To achieve this I've implemented a dep. property and added a style definition.
Below is the part from the view file:
<Style x:Key="SomeKey" TargetType={x:Type DataGridRow} BasedOn= "SomeOtherKey">
<Setter Property="vm:DataGridBehavior:OnDoubleClick" Value="{Binding Path=CommandName,
RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}">
</Setter>
</Style>
the attached property hooks to the double-click event on the data row
and executes the relay command bound to the view.
I'd like to enable more flexibility in the style
by specifying the command-to-be-invoked name in the view itself (as it may differ between different views).
How can I achieve that?
Is there any template definition that I'm missing?
Any help would be greatly appreciated :)
You could subclass DataGrid and add a Property for the Command, e.g MyCommand. Then you could bind that ICommand in each DataGrid and use MyCommand as Path in the RowStyle Binding
DataGrids
<local:CommandDataGrid RowStyle="{StaticResource SomeKey}"
MyCommand="{Binding CommandName1}">
<!-- ... -->
<local:CommandDataGrid RowStyle="{StaticResource SomeKey}"
MyCommand="{Binding CommandName2}">
RowStyle
<Style x:Key="SomeKey" TargetType="{x:Type DataGridRow}">
<Setter Property="vm:DataGridBehavior.OnDoubleClick"
Value="{Binding Path=MyCommand,
RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"/>
</Style>
CommandDataGrid
public class CommandDataGrid : DataGrid
{
public static readonly DependencyProperty MyCommandProperty =
DependencyProperty.Register("MyCommand",
typeof(ICommand),
typeof(CommandDataGrid),
new UIPropertyMetadata(null));
public ICommand MyCommand
{
get { return (ICommand)GetValue(MyCommandProperty); }
set { SetValue(MyCommandProperty, value); }
}
}
Alternatively, you could create an attached property e.g MyCommand that you use
DataGrid
<DataGrid vm:DataGridBehavior.MyCommand="{Binding CommandName}"
RowStyle
<Style x:Key="SomeKey" TargetType="{x:Type DataGridRow}">
<Setter Property="vm:DataGridBehavior.OnDoubleClick"
Value="{Binding Path=(vm:DataGridBehavior.MyCommand),
RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"/>
</Style>
MyCommandProperty
public static readonly DependencyProperty MyCommandProperty =
DependencyProperty.RegisterAttached("MyCommand",
typeof(ICommand),
typeof(DataGridBehavior),
new UIPropertyMetadata(null));
public static void SetMyCommand(DependencyObject element, ICommand value)
{
element.SetValue(MyCommandProperty, value);
}
public static ICommand GetMyCommand(DependencyObject element)
{
return (ICommand)element.GetValue(MyCommandProperty);
}

Resources