How to send output from a custom control to a textbox - c#

I have created a button layout for a virtual keyboard to be used in a WPF app, and to make it reusable I figured I could give it a Target dependency property of type TextBox to send the output of the keyboard to. I made a custom control for my keyboard buttons as well, so that they have an Output property.
What would be the best way to append a button's output to the target TextBox's current text content? Should I use events or commands, or perhaps something else?
XAML for output window:
<UserControl x:Class="Project.Views.DataInput.DataInputExample"
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:Project.Views.DataInput"
xmlns:cc="clr-namespace:Project.CustomControls"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBox Text="{Binding ElementName=Keyboard, Path=Output}" />
<cc:Keyboard x:Name="Keyboard" Grid.Row="1" />
</Grid>
</UserControl>
XAML for Keyboard:
<Style TargetType="{x:Type local:Keyboard}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:Keyboard}">
<Border ...>
<Grid x:Name="PART_Keyboard" ...>
<Grid.RowDefinitions>
...
</Grid.RowDefinitions>
<Grid x:Name="FirstRow" ...>
<Grid.ColumnDefinitions>
...
</Grid.ColumnDefinitions>
<local:KeyboardButton Output="q" Click="BtnClick" />
...
</Grid>
...
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
CS for Keyboard:
public class Keyboard : Control
{
public string Output
{
get { return (string)GetValue(OutputProperty); }
set { SetValue(OutputProperty, value); }
}
public static readonly DependencyProperty OutputProperty =
DependencyProperty.Register("Output", typeof(string), typeof(Keyboard), new PropertyMetadata(""));
static Keyboard()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(Keyboard), new FrameworkPropertyMetadata(typeof(Keyboard)));
}
}
CS for KeyboardButton:
public class KeyboardButton : ImageTextButton
{
public string Output
{
get { return (string)GetValue(OutputProperty); }
set { SetValue(OutputProperty, value); }
}
public static readonly DependencyProperty OutputProperty =
DependencyProperty.Register(
"Output",
typeof(string),
typeof(KeyboardButton),
new PropertyMetadata("")
);
static KeyboardButton()
{
// DefaultStyleKeyProperty.OverrideMetadata(typeof(KeyboardButton), new FrameworkPropertyMetadata(typeof(KeyboardButton)));
}
private void BtnClick(object sender, RoutedEventArgs e)
{
var button = (KeyboardButton)sender;
var keyboard = (Keyboard)(button.Parent);
keyboard.Output += button.Output;
}
}

I will surgest, that your virtual keyboard is rather a ContentControl than a button, but that is another thing.
In your costum virtual keyboard control you make a property:
public string EnteredText {get; private set; }
in your virtual keyboard you make sure this property always contains the current entered text. When you when have a window, there you want to show the entered text in a textbox it can be done in following way:
<local:VirtualKeyboard x:Name="MyVirtualKeyboard" />
<TextBox Text="{Binding ElementName=MyVirtualKeyboard, Path=EnteredText}" />
and then by the binding magic the textbox will contain the text the user is entering. And you can use a lot of other type of controls. For instance a TextBlock would be an obivious candidate.
Because KeyboardButtons are placed inside the virtual keyboard control we need a helper method, that also can be handy in many other cases:
public static P FindVisualParent<P>(DependencyObject Dep) where P : DependencyObject
{
while (Dep != null)
{
if ((Dep is Visual) || (Dep is System.Windows.Media.Media3D.Visual3D))
{
if (Dep is P) return (Dep as P);
Dep = VisualTreeHelper.GetParent(Dep);
}
else
{
Dep = LogicalTreeHelper.GetParent(Dep);
}
}
return (null);
}
Then use this method in the KeyboardButton usercontrol:
public override void OnApplyTemplate()
{
// Base call
base.OnApplyTemplate();
// Get the virtual keyboard control
this.virtualKeyboard = FindVisualParent<VirtualKeyboard>(this);
// Set the click event
this.Click += (s,e) => { this.virtualKeyboard.EnteredText += this.Output; };
}
And in the KeyboardButton control also have this defined as a member variable:
private VirtualKeyboard virtualKeyboard;

Related

calling a method from ViewModel when DataContext changes

The situation:
I have a little app that works with fantasy classes. In the example below I boiled it down to the bare bones. In a ComboBox, situated in the Main Window, the user selects a fantasy class (warrior, rogue, mage etc.) from a list loaded from a DB. This information is passed to a UserControl sitting in Main Window which exposes details about the class using MVVM and data binding. All of this works so far.
The DB has a value (in this case Gear) saved as an int which at the moment displays as an int on screen. It's the app's responsibility to parse that to a string.
So the question is: How do I wire up a method in the UserControl's ViewModel to trigger whenever it's associated View has a DataContext (the selected CharacterClass) change?
Main Window:
<Window x:Class="ExampleApp.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:b="http://schemas.microsoft.com/xaml/behaviors"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ExampleApp"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ComboBox Height="22" MinWidth="70"
ItemsSource="{Binding Classes}"
DisplayMemberPath="Name"
SelectedItem="{Binding SelectedClass}"/>
<local:DetailsView Grid.Column="1" DataContext="{Binding SelectedClass}"/>
</Grid>
</Window>
Main Window ViewModel:
namespace ExampleApp
{
class MainWindowViewModel : Observable
{
private ObservableCollection<CharacterClass> _Classes;
private CharacterClass _SelectedClass;
public ObservableCollection<CharacterClass> Classes
{
get { return _Classes; }
set { SetProperty(ref _Classes, value); }
}
public CharacterClass SelectedClass
{
get { return _SelectedClass; }
set { SetProperty(ref _SelectedClass, value); }
}
public MainWindowViewModel()
{
LoadCharacterClasses();
}
private void LoadCharacterClasses()
{
//simulated data retrieval from a DB.
//hardcoded for demo purposes
Classes = new ObservableCollection<CharacterClass>
{
//behold: Gear is saved as an int.
new CharacterClass { Name = "Mage", Gear = 0, Stats = "3,2,1" },
new CharacterClass { Name = "Rogue", Gear = 1, Stats = "2,2,2" },
new CharacterClass { Name = "Warrior", Gear = 2, Stats = "1,2,3" }
};
}
}
}
My CharacterClass definition. Inheriting from Observable which encapsulates INotifyPropertyChanged
namespace ExampleApp
{
public class CharacterClass : Observable
{
private string _Name;
private int _Gear;
private string _Stats;
public string Name
{
get { return _Name; }
set { SetProperty(ref _Name, value); }
}
public int Gear
{
get { return _Gear; }
set { SetProperty(ref _Gear, value); }
}
public string Stats
{
get { return _Stats; }
set { SetProperty(ref _Stats, value); }
}
}
}
Details about the Observable baseclass:
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace ExampleApp
{
public class Observable : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual void SetProperty<T>(ref T member, T val, [CallerMemberName] string propertyName = null)
{
if (object.Equals(member, val)) return;
member = val;
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
The DetailsView UserControl:
<UserControl x:Class="ExampleApp.DetailsView"
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:ExampleApp"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<DataTemplate DataType="{x:Type local:DetailsViewModel}">
<local:DetailsView/>
</DataTemplate>
</UserControl.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel>
<Label Content="Name:"/>
<Label Content="Base Stats"/>
<Label Content="Starting Gear"/>
</StackPanel>
<StackPanel Grid.Column="1">
<Label Content="{Binding Name}"/>
<Label Content="{Binding Stats}"/>
<Label Content="{Binding gearToString}"/>
</StackPanel>
</Grid>
</UserControl>
and finally: the DetailsViewModel:
public class DetailsViewModel : Observable
{
public string GearToString;
//The method I would like to have called whenever the selected
//CharacterClass (DetailsView.DataContext, so to speak) changes.
private void OnCharacterClassChanged(int gearNumber)
{
switch (gearNumber)
{
case 0:
GearToString = "Cloth";
break;
case 1:
GearToString = "Leather";
break;
case 2:
GearToString = "Plate";
break;
default:
GearToString = "*Error*";
break;
}
}
}
I've fiddled around with attempting to have a command fire when the DetailsView Label updates.
Made a failed attempt to convert DetailsViewModel.GearToString to a dependencyproperty.
I've attempted to override Observable's SetProperty inside of DetailsViewModel.
I don't know which, if any of, those attempts would be viable, if I managed to implement them properly (I've only been coding for several months now :))
I could get it to work using DetailsView code-behind, however that's not MVVM'y.
Because you change your DetailViews DataContext via the combobox, you can access the "current" DetailDataContext before the combobox changes SelectedItem.
You can do this right here:
public CharacterClass SelectedClass
{
get { return _SelectedClass; }
set {
_SelectedClass.DoWhatever();
SetProperty(ref _SelectedClass, value);
}
}
Or you can handle the ComboBoxes SelectionChanged event via a command. Your old value is in e.RemovedItem.
private void Selector_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.RemovedItems.Count > 0)
(e.RemovedItems[0] as CharacterClass).DoSomething();
}
I tend to prefer that approach since it can get confusing quickly if you put too much logic in the setters. It leads to chain reactions that are pretty hard to follow and debug.
In general viewmodels communicate with each other via events. In more complex / disconnected situations with the help of an EventAggregator, MessageBus or something similiar.

Collapse Grid Row in WPF

I have created a custom WPF element extended from RowDefinition that should collapse rows in a grid when the Collapsed property of the element is set to True.
It does it by using a converter and a datatrigger in a style to set the height of the row to 0. It is based on this SO Answer.
In the example below, this works perfectly when the grid splitter is over half way up the window. However, when it is less than half way, the rows still collapse, but the first row does not expand. Instead, there is just a white gap where the rows used to be. This can be seen in the image below.
Similarly, if MinHeight or MaxHeight is set on any of the rows that are collapsed, it no longer collapses the row at all. I tried to fix this by adding setters for these properties in the data trigger but it did not fix it.
My question is what can be done differently so that it does not matter about the size of the rows or if MinHeight / MaxHeight are set, it is just able to collapse the rows?
MCVE
MainWindow.xaml.cs
using System;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace RowCollapsibleMCVE
{
public partial class MainWindow : INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private bool isCollapsed;
public bool IsCollapsed
{
get => isCollapsed;
set
{
isCollapsed = value;
OnPropertyChanged();
}
}
}
public class CollapsibleRow : RowDefinition
{
#region Default Values
private const bool COLLAPSED_DEFAULT = false;
private const bool INVERT_COLLAPSED_DEFAULT = false;
#endregion
#region Dependency Properties
public static readonly DependencyProperty CollapsedProperty =
DependencyProperty.Register("Collapsed", typeof(bool), typeof(CollapsibleRow), new PropertyMetadata(COLLAPSED_DEFAULT));
public static readonly DependencyProperty InvertCollapsedProperty =
DependencyProperty.Register("InvertCollapsed", typeof(bool), typeof(CollapsibleRow), new PropertyMetadata(INVERT_COLLAPSED_DEFAULT));
#endregion
#region Properties
public bool Collapsed {
get => (bool)GetValue(CollapsedProperty);
set => SetValue(CollapsedProperty, value);
}
public bool InvertCollapsed {
get => (bool)GetValue(InvertCollapsedProperty);
set => SetValue(InvertCollapsedProperty, value);
}
#endregion
}
public class BoolVisibilityConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length > 0 && values[0] is bool collapsed)
{
if (values.Length > 1 && values[1] is bool invert && invert)
{
collapsed = !collapsed;
}
return collapsed ? Visibility.Collapsed : Visibility.Visible;
}
return Visibility.Collapsed;
}
public object[] ConvertBack(object value, Type[] targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
}
MainWindow.xaml
<Window x:Class="RowCollapsibleMCVE.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:RowCollapsibleMCVE"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<Visibility x:Key="CollapsedVisibilityVal">Collapsed</Visibility>
<local:BoolVisibilityConverter x:Key="BoolVisibilityConverter"/>
<Style TargetType="{x:Type local:CollapsibleRow}">
<Style.Triggers>
<DataTrigger Value="{StaticResource CollapsedVisibilityVal}">
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource BoolVisibilityConverter}">
<Binding Path="Collapsed"
RelativeSource="{RelativeSource Self}"/>
<Binding Path="InvertCollapsed"
RelativeSource="{RelativeSource Self}"/>
</MultiBinding>
</DataTrigger.Binding>
<DataTrigger.Setters>
<Setter Property="MinHeight" Value="0"/>
<Setter Property="Height" Value="0"/>
<Setter Property="MaxHeight" Value="0"/>
</DataTrigger.Setters>
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<CheckBox Content="Collapse Row"
IsChecked="{Binding IsCollapsed}"/>
<Grid Row="1">
<Grid.RowDefinitions>
<local:CollapsibleRow Height="3*" />
<local:CollapsibleRow Height="Auto" />
<local:CollapsibleRow Collapsed="{Binding IsCollapsed}" Height="*" /> <!-- Using [MaxHeight="300"] breaks this completely -->
</Grid.RowDefinitions>
<StackPanel Background="Red"/>
<GridSplitter Grid.Row="1"
Height="10"
HorizontalAlignment="Stretch">
<GridSplitter.Visibility>
<MultiBinding Converter="{StaticResource BoolVisibilityConverter}" >
<Binding Path="IsCollapsed"/>
</MultiBinding>
</GridSplitter.Visibility>
</GridSplitter>
<StackPanel Background="Blue"
Grid.Row="2">
<StackPanel.Visibility>
<MultiBinding Converter="{StaticResource BoolVisibilityConverter}" >
<Binding Path="IsCollapsed"/>
</MultiBinding>
</StackPanel.Visibility>
</StackPanel>
</Grid>
</Grid>
</Window>
All you need is something to cache the height(s) of the visible row. After that, you no longer need converters or to toggle visibility of contained controls.
CollapsibleRow
public class CollapsibleRow : RowDefinition
{
#region Fields
private GridLength cachedHeight;
private double cachedMinHeight;
#endregion
#region Dependency Properties
public static readonly DependencyProperty CollapsedProperty =
DependencyProperty.Register("Collapsed", typeof(bool), typeof(CollapsibleRow), new PropertyMetadata(false, OnCollapsedChanged));
private static void OnCollapsedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if(d is CollapsibleRow row && e.NewValue is bool collapsed)
{
if(collapsed)
{
if(row.MinHeight != 0)
{
row.cachedMinHeight = row.MinHeight;
row.MinHeight = 0;
}
row.cachedHeight = row.Height;
}
else if(row.cachedMinHeight != 0)
{
row.MinHeight = row.cachedMinHeight;
}
row.Height = collapsed ? new GridLength(0) : row.cachedHeight;
}
}
#endregion
#region Properties
public bool Collapsed
{
get => (bool)GetValue(CollapsedProperty);
set => SetValue(CollapsedProperty, value);
}
#endregion
}
XAML
<Window x:Class="RowCollapsibleMCVE.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:RowCollapsibleMCVE"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<CheckBox Content="Collapse Row"
IsChecked="{Binding IsCollapsed}"/>
<Grid Row="1">
<Grid.RowDefinitions>
<local:CollapsibleRow Height="3*" MinHeight="0.0001"/>
<local:CollapsibleRow Collapsed="{Binding IsCollapsed}" Height="Auto" />
<local:CollapsibleRow Collapsed="{Binding IsCollapsed}" Height="*" /> <!-- Using [MinHeight="50" MaxHeight="100"] behaves as expected -->
</Grid.RowDefinitions>
<StackPanel Background="Red"/>
<GridSplitter Grid.Row="1" Height="10" HorizontalAlignment="Stretch" />
<StackPanel Background="Blue" Grid.Row="2" />
</Grid>
</Grid>
</Window>
You should have either a MaxHeight on the collapsable row (the third one in our example) or a MinHeight on the non-collapsable row (the first) adjacent to the splitter. This to ensure the star sized row has a size when you put the splitter all the way up and toggle visibility. Only then it will be able to take over the remaining space.
UPDATE
As #Ivan mentioned in his post, the controls that are contained by collapsed rows will still be focusable, allowing users to access them when they shouldn't.
Admittedly, it could be a pain setting the visibility for all controls by hand, especially for large XAMLs. So let's add some custom behavior to sync the collapsed rows with their controls.
The Problem
First, run the example using the code above, then collapse the bottom rows by checking the checkbox. Now, press the TAB key once and use the ARROW UP key to move the GridSplitter. As you can see, even though the splitter isn't visible, the user can still access it.
The Fix
Add a new file Extensions.cs to host the behavior.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using RowCollapsibleMCVE;
namespace Extensions
{
[ValueConversion(typeof(bool), typeof(bool))]
public class BooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return !(bool)value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return Binding.DoNothing;
}
}
public class GridHelper : DependencyObject
{
#region Attached Property
public static readonly DependencyProperty SyncCollapsibleRowsProperty =
DependencyProperty.RegisterAttached(
"SyncCollapsibleRows",
typeof(Boolean),
typeof(GridHelper),
new FrameworkPropertyMetadata(false,
FrameworkPropertyMetadataOptions.AffectsRender,
new PropertyChangedCallback(OnSyncWithCollapsibleRows)
));
public static void SetSyncCollapsibleRows(UIElement element, Boolean value)
{
element.SetValue(SyncCollapsibleRowsProperty, value);
}
private static void OnSyncWithCollapsibleRows(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is Grid grid)
{
grid.Loaded += (o,ev) => SetBindingForControlsInCollapsibleRows((Grid)o);
}
}
#endregion
#region Logic
private static IEnumerable<UIElement> GetChildrenFromPanels(IEnumerable<UIElement> elements)
{
Queue<UIElement> queue = new Queue<UIElement>(elements);
while (queue.Any())
{
var uiElement = queue.Dequeue();
if (uiElement is Panel panel)
{
foreach (UIElement child in panel.Children) queue.Enqueue(child);
}
else
{
yield return uiElement;
}
}
}
private static IEnumerable<UIElement> ElementsInRow(Grid grid, int iRow)
{
var rowRootElements = grid.Children.OfType<UIElement>().Where(c => Grid.GetRow(c) == iRow);
if (rowRootElements.Any(e => e is Panel))
{
return GetChildrenFromPanels(rowRootElements);
}
else
{
return rowRootElements;
}
}
private static BooleanConverter MyBooleanConverter = new BooleanConverter();
private static void SyncUIElementWithRow(UIElement uiElement, CollapsibleRow row)
{
BindingOperations.SetBinding(uiElement, UIElement.FocusableProperty, new Binding
{
Path = new PropertyPath(CollapsibleRow.CollapsedProperty),
Source = row,
Converter = MyBooleanConverter
});
}
private static void SetBindingForControlsInCollapsibleRows(Grid grid)
{
for (int i = 0; i < grid.RowDefinitions.Count; i++)
{
if (grid.RowDefinitions[i] is CollapsibleRow row)
{
ElementsInRow(grid, i).ToList().ForEach(uiElement => SyncUIElementWithRow(uiElement, row));
}
}
}
#endregion
}
}
More Testing
Change the XAML to add the behavior and some textboxes (which are also focusable).
<Window x:Class="RowCollapsibleMCVE.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:RowCollapsibleMCVE"
xmlns:ext="clr-namespace:Extensions"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<CheckBox Content="Collapse Row" IsChecked="{Binding IsCollapsed}"/>
<!-- Set the desired behavior through an Attached Property -->
<Grid ext:GridHelper.SyncCollapsibleRows="True" Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="3*" MinHeight="0.0001" />
<local:CollapsibleRow Collapsed="{Binding IsCollapsed}" Height="Auto" />
<local:CollapsibleRow Collapsed="{Binding IsCollapsed}" Height="*" />
</Grid.RowDefinitions>
<StackPanel Background="Red">
<TextBox Width="100" Margin="40" />
</StackPanel>
<GridSplitter Grid.Row="1" Height="10" HorizontalAlignment="Stretch" />
<StackPanel Grid.Row="2" Background="Blue">
<TextBox Width="100" Margin="40" />
</StackPanel>
</Grid>
</Grid>
</Window>
In the end:
The logic is completely hidden from XAML (clean).
We're still providing flexibility:
For each CollapsibleRow you could bind Collapsed to a different variable.
Rows that don't need the behavior can use base RowDefinition (apply on demand).
UPDATE 2
As #Ash pointed out in the comments, you can use WPF's native caching to store the height values. Resulting in very clean code with autonomous properties, each handling its own => robust code. For example, using the code below you won't be able to move the GridSplitter when rows are collapsed, even without the behavior being applied.
Of course the controls would still be accessible, allowing the user to trigger events. So we'd still need the behavior, but the CoerceValueCallback does provide a consistent linkage between the Collapsed and the various height dependency properties of our CollapsibleRow.
public class CollapsibleRow : RowDefinition
{
public static readonly DependencyProperty CollapsedProperty;
public bool Collapsed
{
get => (bool)GetValue(CollapsedProperty);
set => SetValue(CollapsedProperty, value);
}
static CollapsibleRow()
{
CollapsedProperty = DependencyProperty.Register("Collapsed",
typeof(bool), typeof(CollapsibleRow), new PropertyMetadata(false, OnCollapsedChanged));
RowDefinition.HeightProperty.OverrideMetadata(typeof(CollapsibleRow),
new FrameworkPropertyMetadata(new GridLength(1, GridUnitType.Star), null, CoerceHeight));
RowDefinition.MinHeightProperty.OverrideMetadata(typeof(CollapsibleRow),
new FrameworkPropertyMetadata(0.0, null, CoerceHeight));
RowDefinition.MaxHeightProperty.OverrideMetadata(typeof(CollapsibleRow),
new FrameworkPropertyMetadata(double.PositiveInfinity, null, CoerceHeight));
}
private static object CoerceHeight(DependencyObject d, object baseValue)
{
return (((CollapsibleRow)d).Collapsed) ? (baseValue is GridLength ? new GridLength(0) : 0.0 as object) : baseValue;
}
private static void OnCollapsedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
d.CoerceValue(RowDefinition.HeightProperty);
d.CoerceValue(RowDefinition.MinHeightProperty);
d.CoerceValue(RowDefinition.MaxHeightProperty);
}
}
The sample above is technically wrong.
What it essentially does is that it tries to force the height of the row to be 0, which is not what you want or should do - the problem is that the tab key will go through the controls even if the height is 0, and Narrator will read those controls. Essentially those controls still exist and are completely clickable, functional and accessible just they are not presented on the window, but they can be still accessed in various ways and may affect the work of the application.
Second (and the thing that causes the problems that you describe as you did not describe the problems above though they are essential too and should not be ignored), you have GridSplitter and as said it remains functional even if you force its height to 0 (as explained above). GridSplitter means that at the end of the day you are not in the control of the layout, but the user.
What should be done instead is that you should use the plain RowDefinition and set its height to Auto and then set the Visibility of the content of the rows to Collapsed - of course you may use data binding and the converter.
EDIT: further clarification - in the code above you set the new properties called Collapsed and InvertCollapsed. Just because they are named like that they don't have any effect on the row being collapsed, they could be as well called Property1 and Property2. They are used in the DataTrigger in a fairly strange way - when their value is changed that value is converted to Visibility and then if that converted value is Collapsed the setters that force row height to be 0 are called. So someone played a lot of scenery to make it look like that he is collapsing something, but he does not, he only changes the height which is quite different thing to do. And that's where the problems originate from. I certainly suggest to avoid whole this approach, but if you find it is good for your application the minimal thing you need to do is to avoid that approach for the second row where GridSplitter is set up as if you don't your request becomes impossible.

How do I Access Buttons inside a UserControl from xaml?

At work I have several pages, each with buttons in the same places, and with the same properties. Each page also has minor differences. To that end, we created a userControl Template and put all the buttons in it, then applied that user control to all the pages. However, now it's rather hard to access the buttons and modify them from each page's xaml, because they are inside a UserControl on the page..... How do I elegantly access the buttons from each page?
What I've tried:
Currently, we bind to a bunch of dependency properties. I don't like this option because I have a lot of buttons, and need to control a lot of properties on those buttons. The result is hundreds of dependency properties, and a real mess to wade through when we need to change something.
Another method is to use styles. I like this method generally, but because these buttons are inside another control it becomes difficult to modify them, and the template would only be exactly right for one button, at one time.
Adam Kemp posted about letting the user just insert their own button here, and this is the method I'm currently trying to impliment / modify. Unfortunately, I don't have access to Xamarin.
Although the template is inserted when the code runs, the template is not updating the button correctly. If I put a breakpoint in the MyButton Setter, I can see that value is actually an empty button, rather than the one I assigned in my main window. How do I fix this?
Here's some simplified Code:
My Template UserControl's xaml:
<UserControl x:Class="TemplateCode.Template"
x:Name="TemplatePage"
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="350"
d:DesignWidth="525"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Background="DarkGray">
<Grid>
<Button x:Name="_button" Width="200" Height="100" Content="Template Button"/>
</Grid>
</UserControl>
My Template UserControl's Code Behind:
using System.Windows.Controls;
namespace TemplateCode
{
public partial class Template : UserControl
{
public static Button DefaultButton;
public Template()
{
InitializeComponent();
}
public Button MyButton
{
get
{
return _button;
}
set
{
_button = value; //I get here, but value is a blank button?!
// Eventually, I'd like to do something like:
// Foreach (property in value)
// {
// If( value.property != DefaultButton.property) )
// {
// _button.property = value.property;
// }
// }
// This way users only have to update some of the properties
}
}
}
}
And now the application where I want to use it:
<Window x:Class="TemplateCode.MainWindow"
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"
xmlns:templateCode="clr-namespace:TemplateCode"
Title="MainWindow"
Height="350"
Width="525"
Background="LimeGreen"
DataContext="{Binding RelativeSource={RelativeSource Self}}" >
<Grid>
<templateCode:Template>
<templateCode:Template.MyButton>
<Button Background="Yellow"
Content="Actual Button"
Width="200"
Height="100"/>
</templateCode:Template.MyButton>
</templateCode:Template>
</Grid>
</Window>
And Now the Code Behind:
Using System.Windows;
Namespace TemplateCode
{
Public partial class MainWindow : Window
{
Public MainWindow()
{
InitializeComponent();
}
}
}
Edit: While I want to remove unnecessary dependency properties in the template userControl, I'd still like to set bindings on the button's properties from the XAML.
rather than use many dependency properties, prefer style approach. Style contains every property available for a Button control.
I would create a DependencyProperty for each button style in UserControl.
public partial class TemplateUserControl : UserControl
{
public TemplateUserControl()
{
InitializeComponent();
}
public static readonly DependencyProperty FirstButtonStyleProperty =
DependencyProperty.Register("FirstButtonStyle", typeof (Style), typeof (TemplateUserControl));
public Style FirstButtonStyle
{
get { return (Style)GetValue(FirstButtonStyleProperty); }
set { SetValue(FirstButtonStyleProperty, value); }
}
public static readonly DependencyProperty SecondButtonStyleProperty =
DependencyProperty.Register("SecondButtonStyle", typeof (Style), typeof (TemplateUserControl));
public Style SecondButtonStyle
{
get { return (Style)GetValue(SecondButtonStyleProperty); }
set { SetValue(SecondButtonStyleProperty, value); }
}
}
and then modify xaml for buttons to pick these styles:
<UserControl x:Class="MyApp.TemplateUserControl"
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="200" d:DesignWidth="300"
Background="DarkGray">
<StackPanel>
<Button x:Name="_button" Width="200" Height="100"
Style="{Binding Path=FirstButtonStyle, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
<Button x:Name="_button2" Width="200" Height="100"
Style="{Binding Path=SecondButtonStyle, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
</StackPanel>
</UserControl>
now when buttons have to be customized, that can achieved by custom styles:
<StackPanel>
<StackPanel.Resources>
<!--common theme properties-->
<Style TargetType="Button" x:Key="TemplateButtonBase">
<Setter Property="FontSize" Value="18"/>
<Setter Property="Foreground" Value="Blue"/>
</Style>
<!--unique settings of the 1st button-->
<!--uses common base style-->
<Style TargetType="Button" x:Key="BFirst" BasedOn="{StaticResource TemplateButtonBase}">
<Setter Property="Content" Value="1st"/>
</Style>
<Style TargetType="Button" x:Key="BSecond" BasedOn="{StaticResource TemplateButtonBase}">
<Setter Property="Content" Value="2nd"/>
</Style>
</StackPanel.Resources>
<myApp:TemplateUserControl FirstButtonStyle="{StaticResource BFirst}"
SecondButtonStyle="{StaticResource BSecond}"/>
</StackPanel>
You could register a Dependency Property Button on your UserControland handle the initialization in its PropertyChangedCallback.
Template.xaml.cs
using System;
using System.Windows;
using System.Windows.Controls;
using System.Collections.Generic;
using System.Windows.Markup.Primitives;
namespace TemplateCode
{
public partial class Template : UserControl
{
public Template()
{
InitializeComponent();
}
public static readonly DependencyProperty ButtonProperty =
DependencyProperty.Register("Button", typeof(Button), typeof(Template),
new UIPropertyMetadata(new PropertyChangedCallback(ButtonChangedCallback)));
public Button Button
{
get { return (Button)GetValue(ButtonProperty); }
set { SetValue(ButtonProperty, value); }
}
public static List<DependencyProperty> GetDependencyProperties(Object element)
{
List<DependencyProperty> properties = new List<DependencyProperty>();
MarkupObject markupObject = MarkupWriter.GetMarkupObjectFor(element);
if (markupObject != null)
{
foreach (MarkupProperty mp in markupObject.Properties)
{
if (mp.DependencyProperty != null)
{
properties.Add(mp.DependencyProperty);
}
}
}
return properties;
}
private static void ButtonChangedCallback(object sender, DependencyPropertyChangedEventArgs args)
{
// Get button defined by user in MainWindow
Button userButton = (Button)args.NewValue;
// Get template button in UserControl
UserControl template = (UserControl)sender;
Button templateButton = (Button)template.FindName("button");
// Get userButton props and change templateButton accordingly
List<DependencyProperty> properties = GetDependencyProperties(userButton);
foreach(DependencyProperty property in properties)
{
if (templateButton.GetValue(property) != userButton.GetValue(property))
{
templateButton.SetValue(property, userButton.GetValue(property));
}
}
}
}
}
Template.xaml
UserControl DataContext is inherited from parent, no need not to set it explicitly
<UserControl x:Class="TemplateCode.Template"
x:Name="TemplatePage"
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="350"
d:DesignWidth="525"
Background="DarkGray">
<Grid>
<Button x:Name="button" Width="200" Height="100" Content="Template Button"/>
</Grid>
</UserControl>
MainWindow.xaml
You were setting Button.Content instead of Button
<Window x:Class="TemplateCode.MainWindow"
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"
xmlns:templateCode="clr-namespace:TemplateCode"
Title="MainWindow"
Height="350"
Width="525">
<Window.Resources>
<Button x:Key="UserButton"
Background="Yellow"
Content="Actual Button"
Width="200"
Height="100"
/>
</Window.Resources>
<Grid>
<templateCode:Template Button="{StaticResource UserButton}"/>
</Grid>
</Window>
EDIT - Binding Button.Content
3 ways to do this:
1. Dependency Properties
By far the best method. Creating UserControl DP's for every property on the Button is certainly overkill, but for those you want bound to the ViewModel / MainWindow DataContext it makes sense.
Adding in Template.xaml.cs
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string), typeof(Template));
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
Template.xaml
<UserControl x:Class="TemplateCode.Template"
...
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Grid>
<Button x:Name="button" Width="200" Height="100" Content="{Binding Text}"/>
</Grid>
</UserControl>
MainWindow.xaml
<Window.Resources>
<Button x:Key="UserButton"
Background="Yellow"
Width="200"
Height="100"
/>
</Window.Resources>
<Grid>
<templateCode:Template
Button="{StaticResource UserButton}"
Text="{Binding DataContext.Txt,
RelativeSource={RelativeSource AncestorType={x:Type Window}}}"/>
</Grid>
Or
<Window.Resources>
<Button x:Key="UserButton"
Background="Yellow"
Content="Actual Button"
Width="200"
Height="100"
/>
</Window.Resources>
<Grid>
<templateCode:Template
Button="{StaticResource UserButton}"/>
</Grid>
Value precedence: UserButton Content > DP Text, so setting the Content in Resources wins.
2. Creating the Button in your ViewModel
MVVM purists won't like this, but you could use the Binding mark up instead of StaticResource.
MainWindow.xaml
<Grid>
<templateCode:Template
Button="{Binding DataContext.UserButton,
RelativeSource={RelativeSource AncestorType={x:Type Window}}}"/>
</Grid>
3. Setting the binding in code
As you already noticed, a ViewModel prop (e.g. Txt) can't be referenced in Resources because of the order everything is initialized. You can still do it in code later, but it gets a bit messy with the error to prove.
System.Windows.Data Error: 4 : Cannot find source for binding with
reference 'RelativeSource FindAncestor,
AncestorType='System.Windows.Window', AncestorLevel='1''.
BindingExpression:Path=DataContext.Txt; DataItem=null; target element
is 'Button' (Name=''); target property is 'Content' (type 'Object')
Note you need to define the full path on the Content property (setting DataContext on parent won't do).
MainWindow.xaml
<Window.Resources>
<Button x:Key="UserButton"
Background="Yellow"
Width="200"
Height="100"
Content="{Binding DataContext.Txt,
RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
/>
</Window.Resources>
<Grid>
<templateCode:Template Button="{StaticResource UserButton}"/>
</Grid>
Template.xaml.cs
private static void ButtonChangedCallback(object sender, DependencyPropertyChangedEventArgs args)
{
// Get button defined by user in MainWindow
Button userButton = (Button)args.NewValue;
// Get template button in UserControl
UserControl template = (UserControl)sender;
Button templateButton = (Button)template.FindName("button");
// Get userButton props and change templateButton accordingly
List<DependencyProperty> properties = GetDependencyProperties(userButton);
foreach (DependencyProperty property in properties)
{
if (templateButton.GetValue(property) != userButton.GetValue(property))
templateButton.SetValue(property, userButton.GetValue(property));
}
// Set Content binding
BindingExpression bindingExpression = userButton.GetBindingExpression(Button.ContentProperty);
if (bindingExpression != null)
templateButton.SetBinding(Button.ContentProperty, bindingExpression.ParentBinding);
}
If you can group your changes to your buttons to one or multiple properties on your datacontext, you could work with DataTriggers:
<Button x:Name="TestButton">
<Button.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding IsButtonEnabled}" Value="True">
<Setter TargetName="TestButton" Property="Background" Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
You can even use multiple conditions with MultiDataTriggers.
The main problem is that Template components are initialized before mainwindow components.I mean all properties of the button in mainwindow are set after the button in template class is initialized. Therefore, as you said value sets to null. All I want to say is about sequence of initializing objects.If you make a trick such a way as follows ;
public partial class Template : UserControl
{
private Button _btn ;
public Template()
{
}
public Button MyButton
{
get
{
return _button;
}
set
{
_btn = value;
_button = value;
}
}
protected override void OnInitialized(EventArgs e)
{
InitializeComponent();
base.OnInitialized(e);
this._button.Content = _btn.Content;
this._button.Background = _btn.Background;
this.Width = _btn.Width;
this.Height = _btn.Height;
}
}
It is going to work undoubtly.
Another Option based on #Funk's answer is to make a content control instead of a button on the template, then bind the content control's content to your ButtonProperty in the code behind:
on the template:
<ContentControl Content={Binding myButton} Width="200" Height="100"/>
in the template code behind:
public static readonly DependencyProperty myButtonProperty =
DependencyProperty.Register("Button", typeof(Button), typeof(Template),
new UIPropertyMetadata(new PropertyChangedCallback(ButtonChangedCallback)));
and then on the Main Window:
<Window.Resources>
<Button x:Key="UserButton"
Background="Yellow"
Content="Actual Button"
/>
</Window.Resources>
<Grid>
<templateCode:Template myButton="{StaticResource UserButton}"/>
</Grid>
The nice thing about this is that Visual Studio is smart enough to show this code at design time, as well as having less code overall.
You can set things constant things (like location, font, and coloring) for your button either on the content control or in a default style, and then modify just the parts you need for you button.
One option is to simply start writing C# on the xaml page using < ![CDATA[ *** ]]>
In the Main Window.xaml you change to:
<templateCode:Template x:Name="test">
<x:Code><![CDATA[
Void OnStartup()
{
test.MyButton.Content="Actual Button";
test.MyButton.Background = new SolidColorBrush(Color.FromArgb(255,255,255,0));
}
]]>
</x:Code>
Then right after Initialize Object() you call OnStartup().
Although this does let you edit specific properties in the xaml, this is about the same as just writing the code in the code behind, where others expect it to be.

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.

User controls are not retaining the old values when loading through ListBox selecteditem in wpf mvvm

I have recently started using WPF and MVVM approach. I ma getting a problem in the following case. Can someone help me on this?
I have a list box in my MainWindow.xaml. I am trying to load the different user controls for each list box item selection. My MainWindow.xaml looks like below.
MainWindow.xaml
<Window x:Class="MoreOnBinding.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MoreOnBinding"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.Resources>
<DataTemplate DataType="{x:Type local:UserControl1VM}">
<local:UserControl1/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:UserControl2VM}">
<local:UserControl2/>
</DataTemplate>
</Grid.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
</Grid.RowDefinitions>
<ListBox Grid.Column="0" ItemsSource="{Binding ViewModelsCollection}" DisplayMemberPath="ViewModelName" SelectedValue="{Binding SelectedViewModel}" IsSynchronizedWithCurrentItem="True"/>
<ContentControl Grid.Column="1" Content="{Binding SelectedViewModel.ViewModel}"/>
</Grid>
</Window>
My MainWidnow View model is as below.
MainWindowVM
public class MainWindowVM : ViewModelBase
{
public MainWindowVM()
{
this.ViewModelsCollection = new ObservableCollection<ViewModelInfo>(new List<ViewModelInfo>()
{
new ViewModelInfo("Control1", new UserControl1VM()),
new ViewModelInfo("Control2", new UserControl2VM()),
});
}
private ObservableCollection<ViewModelInfo> viewModelsCollection;
public ObservableCollection<ViewModelInfo> ViewModelsCollection
{
get { return viewModelsCollection; }
set
{
if (viewModelsCollection != value)
{
viewModelsCollection = value;
RaisePropertyChanged(() => ViewModelsCollection);
this.SelectedViewModel = this.ViewModelsCollection[0];
}
}
}
private ViewModelInfo selectedViewModel;
public ViewModelInfo SelectedViewModel
{
get { return selectedViewModel; }
set
{
if (selectedViewModel != value)
{
selectedViewModel = value;
RaisePropertyChanged(() => SelectedViewModel);
}
}
}
}
public class ViewModelInfo : ViewModelBase
{
public ViewModelInfo(string viewModelName, ViewModelBase viewModel)
{
this.ViewModelName = viewModelName;
this.ViewModel = viewModel;
}
private string viewModelName;
public string ViewModelName
{
get { return viewModelName; }
set
{
if (viewModelName != value)
{
viewModelName = value;
RaisePropertyChanged(() => ViewModelName);
}
}
}
private ViewModelBase viewModel;
public ViewModelBase ViewModel
{
get { return viewModel; }
set
{
if (viewModel != value)
{
viewModel = value;
RaisePropertyChanged(() => ViewModel);
}
}
}
}
Each user control has a text box in it. The user control xaml and viewmodels are as below.
UserControl1
<UserControl x:Class="MoreOnBinding.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"
xmlns:local="clr-namespace:MoreOnBinding"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Text="User Control 1"/>
<TextBox Grid.Row="2" Text="{Binding UC1Text}" Width="100" Height="30"/>
</Grid>
</UserControl>
UserControl1VM
public class UserControl1VM : ViewModelBase
{
private string uC1Text;
public string UC2Text
{
get { return uC1Text; }
set
{
if (uC1Text != value)
{
uC1Text = value;
RaisePropertyChanged(() => UC2Text);
}
}
}
}
The user control1 code file is as below
public partial class UserControl1 : UserControl
{
public UserControl1()
{
InitializeComponent();
}
}
The UserControl 2 is same as the user control1 only the name change.
Now coming to my problem, after running the application I clicked the first item of the list box and it loaded the UserControl1 on ContentControl. I have enntered some text say "UC1" in the text box displayed. However if click on the second item of the listbox and agian click on the first item, the text I have entered is lost. I want to retain old text. Can someone look into this and help me out?
You have a typo: data source property is UserControl1VM.UC2Text, and the binding is to UC1Text. If you'll look at output window, you'll see a binding error.

Categories

Resources