WPF UserControl with nullable dependency property dependent on checkbox value - c#

I'm trying to create a custom UserControl that displays the properties of a complex object as a form. Additionally, if the user unchecks a checkbox in the header of the UserControl, the value of the dependency property should be null (but the form values should stay displayed, even if the form is disabled) - and vice versa.
I'm working with two dependency properties of type ComplexObject on the UserControl - one is public (this will be bound to by the client), another is private (its properties will be bound to the internal controls in the UserControl):
public ComplexObject ComplexObject
{
get { return (ComplexObject )GetValue(ComplexObjectProperty); }
set { SetValue(ComplexObjectProperty, value); }
}
private ComplexObject VisibleComplexObject
{
get { return (ComplexObject)GetValue(VisibleComplexObjectProperty); }
set { SetValue(VisibleComplexObjectProperty, value); }
}
Now I'm struggling with a binding between those two, so that CompexObject becomes either VisibleComplexObject or null based on the checkbox value. This should also work the other way. I've tried to solve this using DataTriggers in the Style of the UserControl, but was unable to do so:
<UserControl.Style>
<Style TargetType="local:CheckableComplexTypeGroup">
// 'CheckableComplexTypeGroup' TargetType does not match type of the element 'UserControl'
</Style>
</UserControl.Style>
Using <local:CheckableComplexTypeGroup.Style> instead of <UserControl.Style> didn't work either.
Are there any other suggestions? Or maybe another way of doing this?

Finally I've solved this without using plain old event handlers instead of binding/triggers.
CheckableComplexObjectGroup.xaml:
<UserControl Name="thisUC" ...>
<GroupBox>
<GroupBox.Header>
<CheckBox Name="cbComplexObject"
IsChecked="{Binding ElementName=thisUC, Path=ComplexObject, Mode=OneWay, Converter={StaticResource nullToFalseConv}}"
Checked="cbComplexObject_Checked" Unchecked="cbComplexObject_Unchecked"/>
</GroupBox.Header>
...
</UserControl>
CheckableComplexObjectGroup.cs:
public static readonly DependencyProperty ComplexObjectProperty =
DependencyProperty.Register("ComplexObject",
typeof(ComplexObject),
typeof(CheckableComplexObjectGroup),
new PropertyMetadata(null));
private static readonly DependencyProperty VisibleComplexObjectProperty =
DependencyProperty.Register("VisibleComplexObject",
typeof(ComplexObject),
typeof(CheckableComplexObjectGroup),
new PropertyMetadata(new ComplexObject()));
//...
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
if (e.Property == ComplexObjectProperty)
{
if (null != e.NewValue)
VisibleComplexObject = ComplexObject;
}
base.OnPropertyChanged(e);
}
private void cbComplexObject_Checked(object sender, RoutedEventArgs e)
{
ComplexObject = VisibleComplexObject;
}
private void cbComplexObject_Unchecked(object sender, RoutedEventArgs e)
{
ComplexObject = null;
}

Related

WPF two way binding doesn't work until control is modified

I'm trying to synchronize selection in a DataGrid using a collection in my data. I have this mostly working, with one little quirk.
When I change selection in the DataGrid changes are written to my data collection, so far so good. Then, if the data collection changes selection in my DataGrid is updated, as expected. However, if I modify my data before modifying the DataGrid then the DataGrid selection does not update.
An example of the first, working case
An example of the second, non-working case
Code
using System.Collections;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
namespace Testbed
{
public class Widget
{
public string Name { get; set; }
}
public class Data
{
public static Data Instance { get; } = new Data();
public ObservableCollection<Widget> Widgets { get; set; } = new ObservableCollection<Widget>();
public IList SelectedWidgets { get; set; } = new ObservableCollection<Widget>();
Data()
{
Widgets.Add(new Widget() { Name = "Widget 1" });
Widgets.Add(new Widget() { Name = "Widget 2" });
Widgets.Add(new Widget() { Name = "Widget 3" });
}
};
public class BindableDataGrid : DataGrid
{
public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(
"SelectedItems",
typeof(IList),
typeof(BindableDataGrid),
new PropertyMetadata(default(IList)));
public new IList SelectedItems
{
get { return (IList) GetValue(SelectedItemsProperty); }
set { SetValue(SelectedItemsProperty, value); }
}
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
SetCurrentValue(SelectedItemsProperty, base.SelectedItems);
}
}
public partial class MainWindow : Window
{
public MainWindow ()
{
InitializeComponent();
}
private void Button1_Click(object sender, RoutedEventArgs e) { Button_Clicked(0); }
private void Button2_Click(object sender, RoutedEventArgs e) { Button_Clicked(1); }
private void Button3_Click(object sender, RoutedEventArgs e) { Button_Clicked(2); }
private void Button_Clicked(int index)
{
Data data = Data.Instance;
Widget widget = data.Widgets[index];
if (data.SelectedWidgets.Contains(widget))
{
data.SelectedWidgets.Remove(widget);
}
else
{
data.SelectedWidgets.Add(widget);
}
}
}
}
And markup
<Window
x:Class="Testbed.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:test="clr-namespace:Testbed"
Title="MainWindow"
Height="480" Width="640"
DataContext="{Binding Source={x:Static test:Data.Instance}}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition MinWidth="210" />
<ColumnDefinition Width="5" />
<ColumnDefinition MinWidth="210" />
<ColumnDefinition Width="5" />
<ColumnDefinition MinWidth="210" />
</Grid.ColumnDefinitions>
<!-- Change selection through data -->
<StackPanel Grid.Column="0">
<Button Content="Select Widget 1" Click="Button1_Click"/>
<Button Content="Select Widget 2" Click="Button2_Click"/>
<Button Content="Select Widget 3" Click="Button3_Click"/>
</StackPanel>
<!-- Current selection in data -->
<DataGrid Grid.Column="2"
ItemsSource="{Binding SelectedWidgets}"
IsReadOnly="true">
</DataGrid>
<!-- Change selection through UI -->
<test:BindableDataGrid Grid.Column="4"
SelectionMode="Extended"
ColumnWidth="*"
ItemsSource="{Binding Widgets}"
SelectedItems="{Binding SelectedWidgets, Mode=TwoWay}"
IsReadOnly="true">
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}">
<Style.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}" Color="CornflowerBlue"/>
</Style.Resources>
</Style>
</DataGrid.RowStyle>
</test:BindableDataGrid>
</Grid>
</Window>
The problem occurs because you do not handle notifications of the BindableDataGrid.SelectedItems collection.
In the first case you do not need to handle them manually because you actually get the SelectedItems collection from the base DataGrid class and pass it to the view model from the OnSelectionChanged method call. The base DataGrid handle notifications of this collection itself.
However, if you click the button first, the SelectedItems property get a new collection and the base DataGrid knows nothing about it.
I think that you need to handle the propertyChangedCallback, and handle notifications of provided collections to update selection in the grid manually. Refer to the following code demonstrating the concept. Note that I have renamed the property for simplicity but still have not debugged it.
public static readonly DependencyProperty SelectedItemsNewProperty = DependencyProperty.Register(
"SelectedItemsNew",
typeof(IList),
typeof(BindableDataGrid), new PropertyMetadata(OnPropertyChanged));
private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
BindableDataGrid bdg = (BindableDataGrid)d;
if (e.OldValue as INotifyCollectionChanged != null)
(e.NewValue as INotifyCollectionChanged).CollectionChanged -= bdg.BindableDataGrid_CollectionChanged;
if (Object.ReferenceEquals(e.NewValue, bdg.SelectedItems))
return;
if( e.NewValue as INotifyCollectionChanged != null )
(e.NewValue as INotifyCollectionChanged).CollectionChanged += bdg.BindableDataGrid_CollectionChanged;
bdg.SynchronizeSelection(e.NewValue as IList);
}
private void BindableDataGrid_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) {
SynchronizeSelection((IList)sender);
}
private void SynchronizeSelection( IList collection) {
SelectedItems.Clear();
if (collection != null)
foreach (var item in collection)
SelectedItems.Add(item);
}
This happens because your new SelectedItems property never updates the base SelectedItems when it is set. The problem with that is, of course, that MultiSelector.SelectedItems is readonly. It was designed specifically not to be set-able - but it was also designed to be updatable.
The reason your code works at all is because when you change the selection via the BindableDataGrid, SelectedWidgets gets replaced with the DataGrid's internal SelectedItemsCollection. After that point, you are adding and removing from that collection, so it updates the DataGrid.
Of course, this doesn't work if you haven't changed the selection yet, because OnSelectionChanged doesn't run until then, so SetCurrentValue is never called, so the binding never updated SelectedWidgets. But that's fine, all you have to do is called SetCurrentValue as part of BindableDataGrid's initialization.
Add this to BindableDataGrid:
protected override void OnInitialized(EventArgs e)
{
base.OnInitialized(e);
SetCurrentValue(SelectedItemsProperty, base.SelectedItems);
}
Be careful, though, because this will still break if you try to set SelectedItems sometime after initialization. It would be nice if you could make it readonly, but that prevents it from being used in data binding. So make sure that your binding uses OneWayToSource not TwoWay:
<test:BindableDataGrid Grid.Column="4"
SelectionMode="Extended"
ColumnWidth="*"
ItemsSource="{Binding Widgets}"
SelectedItems="{Binding SelectedWidgets, Mode=OneWayToSource}"
IsReadOnly="true">
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}">
<Style.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}" Color="CornflowerBlue"/>
</Style.Resources>
</Style>
</DataGrid.RowStyle>
</test:BindableDataGrid>
If you want to insure this never breaks, you can add a CoerceValueCallback to make sure the new SelectedItems is never set to something other than base.SelectedItems:
public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(
"SelectedItems",
typeof(IList),
typeof(BindableDataGrid),
new PropertyMetadata(default(IList), null, (o, v) => ((BindableDataGrid)o).CoerceBindableSelectedItems(v)));
protected object CoerceBindableSelectedItems(object baseValue)
{
return base.SelectedItems;
}
#Drreamer's answer pointed me in the right direction. However, it boiled down to embracing the fact that the source data collection was being replaced by the DataGrid.SelectedItems collection. It ends up bypassing OnPropertyChanged after the first modification because both ends of the binding are actually the same object.
I didn't want the source collection to be replaced so I found another solution that synchronizes the contents of the collections. It has the benefit of being more direct as well.
When SelectedItems is initialized by the DependencyProperty I stash a reference to the source and target collections. I also register for CollectionChanged on the source and override OnSelectionChanged on the target. Whenever one collection changes I clear the other collection and copy the contents over. As another bonus I no longer have to expose my source collection as IList to allow the DependencyProperty to work since I'm not using it after caching off the source.
public class BindableDataGrid : DataGrid
{
public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(
"SelectedItems",
typeof(IList),
typeof(BindableDataGrid),
new PropertyMetadata(OnPropertyChanged));
private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
BindableDataGrid bdg = (BindableDataGrid) d;
if (bdg.initialized) return;
bdg.initialized = true;
bdg.source = (IList) e.NewValue;
bdg.target = ((DataGrid) bdg).SelectedItems;
((INotifyCollectionChanged) e.NewValue).CollectionChanged += bdg.OnCollectionChanged;
}
public new IList SelectedItems
{
get { return (IList) GetValue(SelectedItemsProperty); }
set { SetValue(SelectedItemsProperty, value); }
}
IList source;
IList target;
bool synchronizing;
bool initialized;
private void OnSourceChanged()
{
if (synchronizing) return;
synchronizing = true;
target.Clear();
foreach (var item in source)
target.Add(item);
synchronizing = false;
}
private void OnTargetChanged()
{
if (synchronizing) return;
synchronizing = true;
source.Clear();
foreach (var item in target)
source.Add(item);
synchronizing = false;
}
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
OnSourceChanged();
}
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
OnTargetChanged();
}
}
I'm sure there's is a much more elegant way to solve this, but this is the best I've got right now.

How to properly expose properties of internal controls?

I have a custom control named CustomTreeView which contains a TreeView. I would like to expose the SelectedItem Property of the TreeView to users of the custom control.
For that I tried to add a new dependency property to the custom control and bind that property to the SelectedItem Property of the TreeView.
Unfortunatly I seem to be getting it wrong. Could you take a look?
TreeView.xaml
<UserControl x:Class="Fis.UI.Windows.BacNet.Views.RestructuredView.View.Controls.CustomTreeView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Selected="{Binding ElementName=treeView, Path=SelectedItem}">
<TreeView x:Name="treeView"/>
</UserControl>
TreeView.xaml.cs
public partial class CustomTreeView : UserControl
{
public static readonly DependencyProperty IsSelectedProperty = DependencyProperty.Register(
"Selected", typeof(Node),
typeof(TreeView)
);
public Node Selected
{
get { return (Node)GetValue(IsSelectedProperty); }
set { SetValue(IsSelectedProperty, value); }
}
public TreeView()
{
InitializeComponent();
}
}
Thanks!
Part of your solution is in this answer here. It's a link-only answer with a dead link (or was -- I just improved it), but it does mention the key point.
You can't bind TreeView.SelectedItem. Your dependency property definition is broken in multiple ways and it should be named SelectedItem in accordance with standard WPF practice. Here's the usercontrol code behind, which defines the dependency property along with event handlers as a substitute for the binding.
public partial class CustomTreeView : UserControl
{
public CustomTreeView()
{
InitializeComponent();
}
#region SelectedItem Property
public Node SelectedItem
{
get { return (Node)GetValue(SelectedProperty); }
set { SetValue(SelectedProperty, value); }
}
public static readonly DependencyProperty SelectedProperty =
DependencyProperty.Register(nameof(SelectedItem), typeof(Node), typeof(CustomTreeView),
new FrameworkPropertyMetadata(null,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
Selected_PropertyChanged)
{ DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged });
protected static void Selected_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
(d as CustomTreeView).OnSelectedChanged(e.OldValue);
}
private void OnSelectedChanged(object oldValue)
{
if (SelectedItem != treeView.SelectedItem)
{
var tvi = treeView.ItemContainerGenerator.ContainerFromItem(SelectedItem) as TreeViewItem;
if (tvi != null)
{
tvi.IsSelected = true;
}
}
}
#endregion SelectedItem Property
private void treeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
if (SelectedItem != e.NewValue)
{
SelectedItem = e.NewValue as Node;
}
}
}
And here's the TreeView in the UserControl XAML. I'm omitting ItemsSource and ItemTemplate as irrelevant.
<TreeView
x:Name="treeView"
SelectedItemChanged="treeView_SelectedItemChanged"
/>
And here's the snippet from MainWindow.xaml I used for testing it:
<StackPanel>
<local:CustomTreeView x:Name="treeControl" />
<Label
Content="{Binding SelectedItem.Text, ElementName=treeControl}"
/>
</StackPanel>
My Node class has a Text property and I'm populating the tree via the DataContext to save having to set up another dependency property for the items.

Pass the value from View to the Mainwindow when the View is closed

I have made a Usercontrol based on MVVM. A window(e.g. MainWindow.xaml) calls this Usercontrol, the View of this Usercontrol has a treeview with nodes, child nodes and buttons ('ok', etc...). The user selects a node in the treeview and press the "ok" button on the View. I could read the selected nodes of the treeview in the View.xaml.cs. I have created dependency properties in View.xaml.cs to save the selected treeview item. In the mainwindow.xaml.cs, I am instantiating my usercontrol and calling the dependency property e.g. usercontrol.value where value is the dependency property in the View.
The overall idea is when user selects the treeview node and press ok, the view should be close and the value of the selected treeview item is paased to the Window.
The problem is when I close the view the value of the dependency property get lost and null is returned to the Window
I am new to WPF.
Window.xaml
<Grid>
<view:SystemExplorerView x:Name="MyView"></view:SystemExplorerView>
</Grid>
Window.xaml.cs
public object m_myValue;
public object myValue {
get { return m_myValue; }
set
{
m_myValue = value;
OnPropertyChanged("myValue");
}
}
public Window1()
{
InitializeComponent();
myValue = MyView.Value;
}
View.xaml.cs
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(object), typeof(SystemExplorerView),
new PropertyMetadata(null));
public SystemExplorerView()
{
InitializeComponent();
}
public object Value
{
get { return (object)GetValue(ValueProperty); }
set
{
SetValue(ValueProperty, value);
}
}
private void OKbtnclk(object sender, RoutedEventArgs e)
{
Value = myTreeView.SelectedItem;
Window.GetWindow(this).Close();
}
You may access the property in a Closing event handler:
<Window ... Closing="Window_Closing">
...
</Window>
Code behind:
private void Window_Closing(object sender, CancelEventArgs e)
{
myValue = MyView.Value;
}

Call a command on a view model whenever any TextBox in the view changes

I have a few views that each have several XAML TextBox instances. The Text property of each is bound to a value object that represents the visual data model for the view.
<TextBox Text="{Binding Path=SelectedItem.SomeValue, UpdateSourceTrigger=PropertyChanged}"/>
I have about 9 or 10 of these boxes in a form. I have a class (ChangeModel) that keeps track of which forms have been altered (e.g. the user has entered in a new value). The problem is the actual value object that is bound to the TextBox.Text property (in the example that would be SelectedItem.SomeValue) can't access the ChangeModel.
I'd like to easily add a binding in the XML (maybe in the resources section) that will call a command in the view model whenever any TextBox changes. I think I can do this with a DataTrigger statement but I'm not sure how to go about it.
Can anyone describe how to use a data trigger or any other XAML mechanism to alert the view model whenever any TextBox within that view is altered?
Alternatively to that Markus Hütter said you can save a few lines of XAML and write custom behavior like this
public class InvokeCommandOnTextChanged : Behavior<TextBox>
{
public static DependencyProperty CommandProperty =
DependencyProperty.Register("Command", typeof(ICommand), typeof(InvokeCommandOnTextChanged));
public static DependencyProperty CommandParameterProperty =
DependencyProperty.Register("CommandParameter", typeof(object), typeof(InvokeCommandOnTextChanged));
public ICommand Command
{
get { return (ICommand)GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
public object CommandParameter
{
get { return GetValue(CommandParameterProperty); }
set { SetValue(CommandParameterProperty, value); }
}
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.TextChanged += OnTextChanged;
}
protected override void OnDetaching()
{
base.OnDetaching();
this.AssociatedObject.TextChanged -= OnTextChanged;
}
private void OnTextChanged(object sender, TextChangedEventArgs e)
{
var command = this.Command;
var param = this.CommandParameter;
if (command != null && command.CanExecute(param))
{
command.Execute(param);
}
}
}
Then you can use this behavior with your textboxes:
<TextBox>
<i:Interaction.Behaviors>
<b:InvokeCommandOnTextChanged Command="{Binding AddCommand}" />
</i:Interaction.Behaviors>
</TextBox>

WPF: How to bind and update display with DataContext

I'm trying to do the following thing:
I have a TabControl with several tabs.
Each TabControlItem.Content points to PersonDetails which is a UserControl
Each BookDetails has a dependency property called IsEditMode
I want a control outside of the TabControl , named ToggleEditButton, to be updated whenever the selected tab changes.
I thought I could do this by changing the ToggleEditButton data context, by it doesn't seem to work (but I'm new to WPF so I might way off)
The code changing the data context:
private void tabControl1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.Source is TabControl)
{
if (e.Source.Equals(tabControl1))
{
if (tabControl1.SelectedItem is CloseableTabItem)
{
var tabItem = tabControl1.SelectedItem as CloseableTabItem;
RibbonBook.DataContext = tabItem.Content as BookDetails;
ribbonBar.SelectedTabItem = RibbonBook;
}
}
}
}
The DependencyProperty under BookDetails:
public static readonly DependencyProperty IsEditModeProperty =
DependencyProperty.Register("IsEditMode", typeof (bool), typeof (BookDetails),
new PropertyMetadata(true));
public bool IsEditMode
{
get { return (bool)GetValue(IsEditModeProperty); }
set
{
SetValue(IsEditModeProperty, value);
SetValue(IsViewModeProperty, !value);
}
}
And the relevant XAML:
<odc:RibbonTabItem Title="Book" Name="RibbonBook">
<odc:RibbonGroup Title="Details" Image="img/books2.png" IsDialogLauncherVisible="False">
<odc:RibbonToggleButton Content="Edit"
Name="ToggleEditButton"
odc:RibbonBar.MinSize="Medium"
SmallImage="img/edit_16x16.png"
LargeImage="img/edit_32x32.png"
Click="Book_EditDetails"
IsChecked="{Binding Path=IsEditMode, Mode=TwoWay}"/>
...
There are two things I want to accomplish, Having the button reflect the IsEditMode for the visible tab, and have the button change the property value with no code behind (if posible)
Any help would be greatly appriciated.
You can accomplish what you want by binding directly to the TabControl's SelectedItem using the ElementName binding:
<odc:RibbonTabItem Title="Book" Name="RibbonBook">
<odc:RibbonGroup Title="Details" Image="img/books2.png" IsDialogLauncherVisible="False">
<odc:RibbonToggleButton Content="Edit"
Name="ToggleEditButton"
odc:RibbonBar.MinSize="Medium"
SmallImage="img/edit_16x16.png"
LargeImage="img/edit_32x32.png"
Click="Book_EditDetails"
IsChecked="{Binding ElementName=myTabControl, Path=SelectedItem.IsEditMode, Mode=TwoWay}"/>
Where myTabControl is the name of the TabControl (the value of the x:Name property). You shouldn't need to handle the SelectionChanged event anymore to update the DataContext of the button.

Categories

Resources