Binding to SelectedItem of ComboBox in UserControl - c#

I have a UserControl consisting of a ComboBox with a Label. I am looking to update a screen with an instance of this ComboBox and dynamically create UserControls in a StackPanel, based on the SelectedItem value.
I currently have a screen with an instance of this ComboBox and have it binding the following way:
Pseudocode Example (removing unrelated code):
<!-- MyComboBoxExample.xaml -->
<ComboBox x:Name="myComboBox" SelectedValuePath="Key" DisplayMemberPath="Value" ItemsSource="{Binding MyBoxItems}/>
/* MyComboBoxExample.xaml.cs */
public static readonly DependencyProperty MyBoxItemsProperty = DependencyProperty.Register("MyBoxItems", typeof(Dictionary<string, string>),
typeof(MyComboBoxExample), new PropertyMetadata(null));
<!-- MyScreen.xaml -->
<local:MyComboBoxExample x:Name="MyComboBoxExampleInstance" MyBoxItems="{Binding Descriptions}"/>
I am new to WPF and databinding, so not sure the best way to implement this. Basically, on the screen: when MyComboBoxExampleInstance selection changes, dynamically set the controls of a StackPanel on the screen. I am not sure how to properly hook in to the SelectionChanged event of a child object of a UserControl.
Any thoughts, corrections, and (constructive) criticism is appreciated. Thanks for any help in advance.

There are several ways to go about this. Here's one way. It's not necessarily the best way but it's easy to understand.
First, the user control xaml. Note the binding of the ItemsSource property on the user control, which specifies MyComboBoxItems as the items source. More on where that comes from in a bit.
<UserControl x:Class="WpfApp1.MyUserControl"
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:WpfApp1"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<ComboBox Height="Auto" ItemsSource="{Binding MyComboBoxItems}" SelectionChanged="OnSelectionChanged">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Text}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</UserControl>
Now the code-behind, MyUserControl.xaml.cs. We provide an combobox selection changed event handler that in turn raises a custom event, MyComboBoxSelectionChanged, which is defined by the event argument class and delegate handler at the bottom of the code. Our OnSelectionChanged method simply forwards the selection change event via the custom event we've defined.
using System;
using System.Windows.Controls;
namespace WpfApp1
{
/// <summary>
/// Interaction logic for MyUserControl.xaml
/// </summary>
public partial class MyUserControl : UserControl
{
public event MyComboBoxSelectionChangedEventHandler MyComboBoxSelectionChanged;
public MyUserControl()
{
InitializeComponent();
}
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count > 0)
{
MyComboBoxSelectionChanged?.Invoke(this,
new MyComboBoxSelectionChangedEventArgs() {MyComboBoxItem = e.AddedItems[0]});
}
}
}
public class MyComboBoxSelectionChangedEventArgs : EventArgs
{
public object MyComboBoxItem { get; set; }
}
public delegate void MyComboBoxSelectionChangedEventHandler(object sender, MyComboBoxSelectionChangedEventArgs e);
}
Now we go to our MainWindow.xaml, where we define an instance of MyUserControl and set a handler for the custom event we defined. We also provide a StackPanel to host the items that will be created on a selection changed event.
<Window x:Class="WpfApp1.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="450"
Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<local:MyUserControl Width="140" Height="32" DataContext="{Binding}" Grid.Row="0" MyComboBoxSelectionChanged="OnSelectionChanged"></local:MyUserControl>
<StackPanel Grid.Row="1" x:Name="MyUserControls"/>
</Grid>
</Window>
Now the code-behind for MainWindow.xaml. Here we define a public property containing a list of objects of type MyComboBoxItem (defined at the bottom of the file), and we initialize the array with some values.
Recall that we set the ItemsSource property of the ComboBox inside MyUserControl to "{Binding MyComboBoxItems}", so the question is, how does the property defined in the MainWindow magically become available in MyUserControl?
In WPF, DataContext values are inherited from parent controls if they aren't explicitly set, and since we did not specify a data context for the control, the instance of MyUserControl inherits the DataContext of the parent window. In the constructor we set the MainWindow data context to refer to itself, so the MyComboBoxItems list is available to any child controls (and their children, and so on.)
Typically we'd go ahead and add a dependency property for the user control called ItemsSource and in the user control we'd bind the ComboBox's ItemsSource property to the dependency property rather than to MyComboxItems.
MainWindow.xaml would then bind it's collection directly to the dependency property on the user control. This helps make the user control more re-usable since it wouldn't depend on specific properties defined in an inherited data context.
Finally, in the event handler for the user control's custom event we obtain the value selected by the user and create a UserControl populated with a text box (all with various properties set to make the items interesting visually) and we directly add them to the Children property of the StackPanel.
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace WpfApp1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public List<MyComboBoxItem> MyComboBoxItems { get; set; } = new List<MyComboBoxItem>()
{
new MyComboBoxItem() {Text = "Item1"},
new MyComboBoxItem() {Text = "Item2"},
new MyComboBoxItem() {Text = "Item3"},
};
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
private void OnSelectionChanged(object sender, MyComboBoxSelectionChangedEventArgs e)
{
if (e.MyComboBoxItem is MyComboBoxItem item)
{
MyUserControls.Children.Add(
new UserControl()
{
Margin = new Thickness(2),
Background = new SolidColorBrush(Colors.LightGray),
Content = new TextBlock()
{
Margin = new Thickness(4),
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Center,
FontSize = 48,
FontWeight = FontWeights.Bold,
Foreground = new SolidColorBrush(Colors.DarkGreen),
Text = item.Text
}
});
}
}
}
public class MyComboBoxItem
{
public string Text { get; set; }
}
}
Finally, I'd consider using an ItemsControl or a ListBox bound to an ObservableCollection rather than sticking things into a StackPanel. You could define a nice data template for the user control to display and maybe a DataTemplateSelector to use different user controls based on settings in the data item. This would allow me to simply add the reference to the MyComboBoxItem obtained in the selection changed handler to that collection, and the binding machinery would automatically generate a new item using the data template I defined and create the necessary visual elements to display it.
So given all that, here are the changes to do all that.
First, we modify our data item to add a color property. We'll use that property to determine how we display the selected item:
public class MyComboBoxItem
{
public string Color { get; set; }
public string Text { get; set; }
}
Now we implement INotifyPropertyChanged in MainWindow.xaml.cs to let the WPF binding engine update the UI when we change properties. This is the event handler and a helper method, OnPropertyChanged.
We also modify the combo box initializer to add a value for the Color property. We'll leave on blank for fun.
We then add a new ObservableCollect, "ActiveUserControls" to store the MyComboBoxItem received in the combo box selection changed event. We do that instead of creating user controls on the fly in code.
public partial class MainWindow : Window, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public List<MyComboBoxItem> MyComboBoxItems { get; set; } = new List<MyComboBoxItem>()
{
new MyComboBoxItem() {Text = "Item1", Color = "Red"},
new MyComboBoxItem() {Text = "Item2", Color = "Green"},
new MyComboBoxItem() {Text = "Item3"},
};
private ObservableCollection<MyComboBoxItem> _activeUserControls;
public ObservableCollection<MyComboBoxItem> ActiveUserControls
{
get => _activeUserControls;
set { _activeUserControls = value; OnPropertyChanged(); }
}
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
private void OnSelectionChanged(object sender, MyComboBoxSelectionChangedEventArgs e)
{
if (e.MyComboBoxItem is MyComboBoxItem item)
{
if (ActiveUserControls == null)
{
ActiveUserControls = new ObservableCollection<MyComboBoxItem>();
}
ActiveUserControls.Add(item);
}
}
}
Now let's look at some changes we made to MyUserControl. We've modified the combo box ItemsSource to point at a property, ItemsSource defined in MyUserControl, and we also map the ItemTemplate to an ItemTemplate property in MyUserControl.
<UserControl x:Class="WpfApp1.MyUserControl"
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:WpfApp1"
mc:Ignorable="d"
d:DesignHeight="450"
d:DesignWidth="800">
<Grid>
<ComboBox Height="Auto"
ItemsSource="{Binding ItemsSource, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MyUserControl}}}"
ItemTemplate="{Binding ItemTemplate, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MyUserControl}}}"
SelectionChanged="OnSelectionChanged">
</ComboBox>
</Grid>
</UserControl>
Here's were we define those new properties in MyUserControl.cs.
public partial class MyUserControl : UserControl
{
public event MyComboBoxSelectionChangedEventHandler MyComboBoxSelectionChanged;
public MyUserControl()
{
InitializeComponent();
}
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource",
typeof(System.Collections.IEnumerable),
typeof(MyUserControl),
new PropertyMetadata(null));
public System.Collections.IEnumerable ItemsSource
{
get => GetValue(ItemsSourceProperty) as IEnumerable;
set => SetValue(ItemsSourceProperty, (IEnumerable)value);
}
public static readonly DependencyProperty ItemTemplateProperty =
DependencyProperty.Register("ItemTemplate",
typeof(DataTemplate),
typeof(MyUserControl),
new PropertyMetadata(null));
public DataTemplate ItemTemplate
{
get => GetValue(ItemTemplateProperty) as DataTemplate;
set => SetValue(ItemTemplateProperty, (DataTemplate)value);
}
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count > 0)
{
MyComboBoxSelectionChanged?.Invoke(this,
new MyComboBoxSelectionChangedEventArgs() {MyComboBoxItem = e.AddedItems[0]});
}
}
}
Let's look at how we bind to those in MainWindow.xaml:
<local:MyUserControl Width="140"
Height="32"
Grid.Row="0"
MyComboBoxSelectionChanged="OnSelectionChanged"
ItemsSource="{Binding MyComboBoxItems}"
ItemTemplate="{StaticResource ComboBoxItemDataTemplate}" />
So now we can bind our items directly and provide our own data template to specify how the combobox should display the item.
Finally, I want to replace the StackPanel with an ItemsControl. This is like a ListBox without scrolling or item selection support. In fact, ListBox is derived from ItemsControl. I also want to use a different user control in the list based on the value of the Color property. To do that, we define some data templates for each value in MainWindow.Xaml:
<DataTemplate x:Key="ComboBoxItemDataTemplate"
DataType="local:MyComboBoxItem">
<StackPanel Orientation="Horizontal">
<TextBlock Margin="4"
Text="{Binding Text}" />
<TextBlock Margin="4"
Text="{Binding Color}" />
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="GreenUserControlDataTemplate"
DataType="local:MyComboBoxItem">
<local:GreenUserControl DataContext="{Binding}" />
</DataTemplate>
<DataTemplate x:Key="RedUserControlDataTemplate"
DataType="local:MyComboBoxItem">
<local:RedUserControl DataContext="{Binding}" />
</DataTemplate>
<DataTemplate x:Key="UnspecifiedUserControlDataTemplate"
DataType="local:MyComboBoxItem">
<TextBlock Margin="4"
Text="{Binding Text}" />
</DataTemplate>
Here's RedUserControl. Green is the same with a different foreground color.
<UserControl x:Class="WpfApp1.RedUserControl"
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:WpfApp1"
mc:Ignorable="d"
d:DesignHeight="450"
d:DesignWidth="800">
<Grid Background="LightGray"
Margin="2">
<TextBlock Margin="4"
Foreground="DarkRed"
TextWrapping="Wrap"
Text="{Binding Text}"
FontSize="24"
FontWeight="Bold" />
</Grid>
</UserControl>
Now the trick is to use the right data template based on the color value. For that we create a DataTemplateSelector. This is called by WPF for each item to be displayed. We can examine the data context object a choose which data template to use:
public class UserControlDataTemplateSelector : DataTemplateSelector
{
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if (container is FrameworkElement fe)
{
if (item is MyComboBoxItem cbItem)
{
if (cbItem.Color == "Red")
{
return fe.FindResource("RedUserControlDataTemplate") as DataTemplate;
}
if (cbItem.Color == "Green")
{
return fe.FindResource("GreenUserControlDataTemplate") as DataTemplate;
}
return fe.FindResource("UnspecifiedUserControlDataTemplate") as DataTemplate;
}
}
return null;
}
}
We create an instance of our data template selector in xaml in MainWindow.xaml:
<Window.Resources>
<local:UserControlDataTemplateSelector x:Key="UserControlDataTemplateSelector" />
...
Finally we replace our stack panel with an Items control:
<ItemsControl Grid.Row="1"
x:Name="MyUserControls"
ItemsSource="{Binding ActiveUserControls}"
ItemTemplateSelector="{StaticResource UserControlDataTemplateSelector}" />

Related

WPF binding source mismatch for a MahApps.Metro.Controls.NumericUpDown inside a TabControl

I often use MahApps.Metro.Controls.NumericUpDown to manipulate numerical values from the user interface. Recently I have observed a binding behaviour so strange and surreal that completely undermined my faith in the WPF binding infrastructure.
The phenomenon occurred with a collection of view models bound to the ItemsSource of a TabControl. Within each of these view models, there is a property of type int that I bind to the Value property of a MahApps.Metro.Controls.NumericUpDown. I noticed that when I manipulate a NumericUpDown through its TextBox and leave the focus there just before I select a new tab in the TabControl, the value I have just set gets written into the NumericUpDown inside the new tab being selected. That is clearly undesirable as that tab contains a value I did not intend to overwrite just by selecting the containing tab.
This only occurs if I use the TextBox part to set the value, and not when I use the + and - buttons. Furthermore, taking the focus from the NumericUpDown prevents the bug from occurring. Hooking into the LostFocus event of the NumericUpDown, I could observe that the binding source of the respective binding had already changed to that within the view model associated with the next tab (instead of that associated with the NumericUpDown actually firing the LostFocus event), and that explains the surreal value bleed. (Additionally, it gets fired twice, as you will be able to see in the MWE.)
I could find a workaround by registering for the PreviewKeyDown event of the tab label and setting the focus to another element programmatically before the tab switch actually occurs. Still, as I said, this undermined my confidence in the WPF binding architecture. Is it a general WPF binding bug or is it specific to MahApps.Metro.Controls.NumericUpDown? What can I do to ensure such bugs do not occur in my code (apart from the amateur workaround I could come up with)?
The XAML part of the MWE:
<Window x:Class="NumericUpDownMWE.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:mahApps="http://metro.mahapps.com/winfx/xaml/controls"
xmlns:local="clr-namespace:NumericUpDownMWE"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<TabControl
ItemsSource="{Binding Items}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" VerticalAlignment="Top">
<TextBlock Text="Value" />
<mahApps:NumericUpDown
LostFocus="HandleLostFocus"
Value="{Binding Value}" />
</StackPanel>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
</Window>
The respective code-behind:
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
using System.Windows.Data;
namespace NumericUpDownMWE
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ExampleViewModel();
}
private void HandleLostFocus(object sender, RoutedEventArgs e)
{
FrameworkElement? frameworkElement = sender as FrameworkElement;
BindingExpression? bindingExpression = frameworkElement?.GetBindingExpression(MahApps.Metro.Controls.NumericUpDown.ValueProperty);
string? sourceName = (bindingExpression?.ResolvedSource as ExampleItem)?.Name;
}
}
public class ExampleItem : INotifyPropertyChanged
{
private int value;
public event PropertyChangedEventHandler? PropertyChanged;
public string Name { get; }
public int Value {
get { return value; }
set {
if (value != this.value) {
this.value = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
}
}
}
public ExampleItem(string name, int value)
{
this.Name = name;
this.value = value;
}
}
public class ExampleViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public ObservableCollection<ExampleItem> Items { get; }
public ExampleViewModel()
{
this.Items = new ObservableCollection<ExampleItem>() {
new ExampleItem("Item A", 1),
new ExampleItem("Item B", 2),
new ExampleItem("Item C", 3)
};
}
}
}

WPF and Caliburn Not Binding Nested UserControl

I'm simply trying to create a binding for a UserControl within a UserControl using WPF/Caliburn, but I am having trouble properly binding the nested UserControl.
The basic layout is a ShellViewModel : Conductor, and within the ShellView there is a ContentControl that is populated by the ShellViewModel.ActivateItem method that loads a UserControl (PageViewModel), and within the PageView UserControl there is a nested UserControl called "SimpleControl".
The binding works when the page loads (it displays the "Initial Text Value" string in the nested UserControl), but it does not appear to be bound to the PropertyChanged event on PageView (and never updates its value when the test button is pressed). The Label in the Parent UserControl (PageView) is bound correctly/updates as expected.
PageView.xaml:
<UserControl x:Class="CaliburnTest.Views.PageView"
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:CaliburnTest.Views"
xmlns:cal="http://www.caliburnproject.org"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<StackPanel Background="Aqua">
<Label Content="{Binding TextLabelTest, FallbackValue=DEFAULT}"></Label>
<local:SimpleControl cal:Bind.Model="WPFCaliburnTemplate.Views.PageView" TextValue="{Binding TextLabelTest}"></local:SimpleControl>
<Button Name="UpdateTextButton">Update Text</Button>
</StackPanel>
</UserControl>
PageViewModel.cs:
using Caliburn.Micro;
namespace CaliburnTest.ViewModels
{
public class PageViewModel : Screen
{
private string _textLabelTest;
public string TextLabelTest
{
get { return _textLabelTest; }
set
{
_textLabelTest = value;
NotifyOfPropertyChange(() => TextLabelTest);
}
}
public PageViewModel()
{
TextLabelTest = "Initial Text Value";
}
public void UpdateTextButton()
{
TextLabelTest = "Updated Text Value";
}
}
}
SimpleControl.xaml:
<UserControl x:Class="CaliburnTest.Views.SimpleControl"
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:cal="clr-namespace:Caliburn.Micro;assembly=Caliburn.Micro.Platform.Core"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Border Margin="10" BorderThickness="1" BorderBrush="#FF9A9A9A">
<StackPanel>
<Label Name="TextLabel"></Label>
</StackPanel>
</Border>
</UserControl>
And finally SimpleControl.xaml.cs:
using System.Windows;
using System.Windows.Controls;
namespace CaliburnTest.Views
{
public partial class SimpleControl : UserControl
{
public SimpleControl()
{
InitializeComponent();
}
public static DependencyProperty TextValueProperty = DependencyProperty.Register("TextValue", typeof(string), typeof(SimpleControl),
new FrameworkPropertyMetadata("", TextValueChangedCallBack));
public string TextValue
{
get { return (string)GetValue(TextValueProperty); }
set
{
SetValue(TextValueProperty, value);
Refresh();
}
}
protected static void TextValueChangedCallBack(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
(sender as SimpleControl).TextValue = (string)e.NewValue;
}
private void Refresh()
{
TextLabel.Content = TextValue;
}
}
}
I have been banging on this all day and cannot figure out what I'm doing wrong. I've tried all combinations of different DataContexts, RelativeSources, and whatever else I can find on SO and Google but am still coming up short. I have a much more complicated custom UserControl I'm trying to work with but I created this simple example to try and figure out the issue.
I played around with all sorts of combinations and I believe I found a result that appears to work, though I'm not entirely sure why:
First, I changed the way I was binding the "TextValue" dependency property to:
<local:SimpleControl TextValue="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type UserControl}}, Path=DataContext.TextLabelTest}" />
However, this was still only working for the initial value and the binding was not updating when the string was updated. In the end, I had to edit SimpleControl.xaml.cs like so:
protected static void TextValueChangedCallBack(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
// (sender as SimpleControl).TextValue = (string)e.NewValue;
(sender as SimpleControl).Refresh();
}
As you can see, I commented out the line setting the SimpleControl.TextValue (and in turn setting the TextValueProperty DependencyProperty) and simply called the SimpleControl.Refresh() method to update the label. I'm not sure why setting the TextValueProperty was breaking the DependencyProperty (even though it was already set when the string updated), but I think I need to go brush up on some MSDN articles!

SelectedItem doesn't update properly when trying to share items between views

I have a scenario where the same collection of items can be viewed in different ways. That is, we have multiple visual representations for the same data. In order to keep our application visually clean you can only view one of these views at a time. The problem I'm having is that if you change the selected item while viewing View #1 then when you switch to View #2 the selected item isn't updating properly.
My steps for reproducing:
On View #1 select Item #1.
Toggle to View #2 - at this point Item #1 is selected
Scroll down to "Item #200" and select it
Toggle back to View #1
Item #1 will still be highlighted and if you scroll down to Item #200 it is also highlighted
It seems like when the listbox is collapsed the selection changes aren't being picked up. What am I missing? Is it expected that the PropertyChanged events won't update the UI elements if they aren't visible?
I have a very simplified version of my code below. Basically, I have a shared array that is being bound to two different ListBox controls.
XAML:
<Window x:Class="SharedListBindingExample.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:SharedListBindingExample"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ListBox Grid.Row="0" x:Name="listBox1" ItemsSource="{Binding List1}">
<ListBox.Resources>
<Style TargetType="ListBoxItem">
<Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}"/>
</Style>
<DataTemplate DataType="{x:Type local:SharedListItem}">
<Grid HorizontalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Button Background="Red" />
<Label Grid.Row="1" Content="{Binding Name}" />
</Grid>
</DataTemplate>
</ListBox.Resources>
</ListBox>
<ListBox Grid.Row="0" x:Name="listBox2" ItemsSource="{Binding List2}" Background="AliceBlue" Visibility="Collapsed">
<ListBox.Resources>
<Style TargetType="ListBoxItem">
<Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}"/>
</Style>
<DataTemplate DataType="{x:Type local:SharedListItem}">
<Label Content="{Binding Name}" HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch"/>
</DataTemplate>
</ListBox.Resources>
</ListBox>
<Button Grid.Row="1" Click="Button_Click">Toggle View</Button>
</Grid>
</Window>
Code Behind:
using System.Windows;
namespace SharedListBindingExample
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
if (listBox1.Visibility == Visibility.Collapsed)
{
listBox1.Visibility = Visibility.Visible;
listBox2.Visibility = Visibility.Collapsed;
}
else
{
listBox2.Visibility = Visibility.Visible;
listBox1.Visibility = Visibility.Collapsed;
}
}
}
}
ViewModel:
using System.Collections.Generic;
namespace SharedListBindingExample
{
public class TwoPropertiesForSameListViewModel
{
private readonly List<SharedListItem> _sharedList;
public TwoPropertiesForSameListViewModel()
{
_sharedList = new List<SharedListItem>();
for (int i = 0; i < 300; i++)
{
_sharedList.Add(new SharedListItem($"Item #{i}"));
}
}
public IEnumerable<SharedListItem> List1
{
get
{
return _sharedList;
}
}
public IEnumerable<SharedListItem> List2
{
get
{
return _sharedList;
}
}
}
}
SharedListItem:
using System.ComponentModel;
namespace SharedListBindingExample
{
public class SharedListItem : INotifyPropertyChanged
{
private bool _isSelected;
public SharedListItem(string name)
{
Name = name;
}
public string Name { get; set; }
public bool IsSelected
{
get
{
return _isSelected;
}
set
{
if (value != _isSelected)
{
_isSelected = value;
OnPropertyChanged("IsSelected");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
I believe you need to use a CollectionViewSource object between the two different views to keep the selected item in sync. I do something similar in my own application.
I have one control which defines a CollectionViewSource resource in the Xaml:
<UserControl.Resources>
<CollectionViewSource x:Key="DataView" />
</UserControlResources>
The control also has a DependencyProperty for the CollectionViewSource that allows it to be Data Bound to other controls:
public static readonly DataViewProperty =
DependencyProperty.Register( "DataView", typeof( CollectionViewSource ), typeof( YourControlType ), new PropertyMetadata( null ) );
public CollectionViewSource DataView {
get { return (CollectionViewSource) GetProperty( DataViewProperty); }
set { SetProperty( DataViewProperty, value );
}
Then in the components constructor, after calling InitializeComponent, you have to execute code like this:
public MyUserControl() {
InitializeComponent();
DataView = FindResource( "DataView" ) as CollectionViewSource;
DataView.Source = YourObservableCollection;
}
In the other view(s) where you want to share this object, you create a new CollectionViewSource DependencyProperty. This allows you to bind the two proeprties to each other in the window that has the different views of your data. In my second control, I have another ObservableCollection object property, but it is not initialized in the control's constructor. What I do is in the control's Loaded event handler, I set that ObservableCollection property's value to the value of the CollectionViewSource object's Source property. That is:
if ( DataCollection == null && DataView != null ) {
DataCollection = (ObservableCollection<DataType>) DataView.Source;
DataGrid.ItemsSource = DataView.View;
}
After this, both controls share the same ObservableCollection and the same CollectionViewSource. It's the CollectionViewSource that keeps the two control's selected item in sync.
Obviously, you can share that CollectionViewSource object across as many views as you like. One control has to declare the object, the others have to share it.
When using WPF, it is recommended to ditch IEnumerables for ObservableCollection or ICollectionView. You can find more details about these collections on MSDN, but my suggestion is to bind SelectedItem of one ListBox to another in XAML (by SelectedItem = {Binding ElementName='yourList', Path='SelectedItem'}), so that when one changes, the other one will respond (you should do this for both lists). I have never tried this cyclic binding myself, but I think it will work fine in your case.

Assigning datacontext to autogenerated tabitem in tabcontrol

I am trying to assign an object to datacontext from TabItem. To get an idea, look at the following code sample
<UserControl x:Class="CustomCopyNas.UserControls.LoginUsers"
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:igWindows="http://infragistics.com/Windows"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid Margin="5,0,5,0">
<igWindows:XamTabControl Name="_xamTabControl"
TabLayoutStyle="MultiRowSizeToFit"
MaximumTabRows="4"
MaximumSizeToFitAdjustment="50"
MinimumTabExtent="100"
InterTabSpacing="2"
InterRowSpacing="2"
Theme="Metro"
AllowTabClosing="False"
TabItemCloseButtonVisibility="WhenSelectedOrHotTracked">
<igWindows:XamTabControl.ContentTemplate>
<DataTemplate>
<TextBox Text="{Binding Prop}"/>
</DataTemplate>
</igWindows:XamTabControl.ContentTemplate>
</igWindows:XamTabControl>
</Grid>
</UserControl>
As you can see, I use datatemplate for TabItem content appearance, TextBox. The TextBox Text property is binding to a property from the datacontext.
And the partial class from UserControl
public class Foo
{
public string Prop {
get { return "Hello Foo"; }
}
}
/// <summary>
/// Interaction logic for LoginUsers.xaml
/// </summary>
public partial class LoginUsers : UserControl
{
public LoginViewModel LoginViewModel = new LoginViewModel("file.xml");
public LoginUsers()
{
InitializeComponent();
foreach (var server in LoginViewModel.ServerUsers)
{
string header = server.Server;
string name = "tabItem" + header;
_xamTabControl.Items.Add(new TabItemEx() { Header = header, Name = name, DataContext = new Foo() });
}
}
}
As output on TabItem content I've got nothing, so emtpy content, why?
You don't seem to have declared your TabControl XAML properly. It is customary to see it defined more like this, using the TabControl.ItemsSource property:
<TabControl ItemsSource="{Binding YourCollectionProperty}">
<TabControl.ItemTemplate> <!-- Header Template-->
<DataTemplate>
<TextBlock Text="{Binding HeaderText}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate> <!-- Body Template-->
<DataTemplate>
<TextBlock Text="{Binding BodyText}" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
For this to work, you'll need to create a custom class that has HeaderText and BodyText properties in it. Then you'll need to create a public ObservableCollection<YourCustomClass> collection property in your code behind named YourCollectionProperty.
Please note that the Bindings inside the two DataTemplates will automatically have their DataContexts set to an item from the YourCollectionProperty collection and that is why your Binding to the Prop property didn't work.
Try moving the foreach loop so that it fires when the _xamTabControl.Loaded event fires. That should do the trick

Bind DependencyProperty of Usercontrol in ListBox

I need ListBox with my UserControl listed in it. My UserControl has TextBox. So I want to display property of List's subitem in UserControl's textBox. I have tried a lot of options with DataContext and ElementName - it just doesn`t work. I just stucked on it. The only way to make it work is to remove DataContext binding of UserControl to itself and change Item Property name so it matches to DependencyProperty name - but I need to reuse my control in different viewmodels with different entities so it is almost not possible to use the approach.
Interesting thing is that if I change my UserControl to Textbox and bind Text property of it - everything works. What the difference between Textbox and my UserControl?
So let me just show my code.
I have simplified the code to show only essential:
Control XAML:
<UserControl x:Class="TestControl.MyControl"
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="100" d:DesignWidth="200"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Grid>
<TextBlock Text="{Binding Text}"/>
</Grid>
</UserControl>
Control CS:
public partial class MyControl : UserControl
{
public MyControl()
{
InitializeComponent();
}
public string Text
{
get {
return (string)this.GetValue(TextProperty); }
set {
this.SetValue(TextProperty, value); }
}
public static DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(MyControl), new propertyMetadata(""));
}
Window XAML:
<Window x:Class="TestControl.MainWindow"
Name="_windows"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TestControl"
Title="MainWindow" Height="350" Width="525" >
<Grid Name="RootGrid">
<ListBox ItemsSource="{Binding ElementName=_windows, Path=MyList}">
<ItemsControl.ItemTemplate >
<DataTemplate >
<local:MyControl Text="{Binding Path=Name}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ListBox>
</Grid>
</Window>
Window CS:
public partial class MainWindow : Window
{
public MainWindow()
{
_list = new ObservableCollection<Item>();
_list.Add(new Item("Sam"));
_list.Add(new Item("App"));
_list.Add(new Item("H**"));
InitializeComponent();
}
private ObservableCollection<Item> _list;
public ObservableCollection<Item> MyList
{
get { return _list;}
set {}
}
}
public class Item
{
public Item(string name)
{
_name = name;
}
private string _name;
public string Name
{
get { return _name; }
set { _name = value; }
}
}
This is a pretty big gotcha in XAML. The problem is that when you do this in the user control:
DataContext="{Binding RelativeSource={RelativeSource Self}}"
You change its data context, so that in this line:
<local:MyControl Text="{Binding Path=Name}"/>
The runtime will now attempt to resolve "Name" on the instance of "MyControl", instead of on the inherited data context (ie, the view model). (Confirm this by checking the Output window -- you should see a binding error to that effect.)
You can get around this by, instead of setting the user control's data context that way, using a RelativeSource binding:
<UserControl x:Class="TestControl.MyControl"
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="100" d:DesignWidth="200"
<Grid>
<TextBlock
Text="{Binding Text,RelativeSource={RelativeSource AncestorType=UserControl}}"
/>
</Grid>
</UserControl>

Categories

Resources