I'm quite new with WPF and I might be missing something simple, but I've tried with some simple examples for Custom User Control Binding and they work, but when I try to apply them to our situation, the binding only works when I modify the value in the Custom User Control, but if I change it from other place the Custom User Control does not reflect that change.
We are using Prism and we have a ViewModel and a View to show and edit the properties of a Device. These properties are read from an XML at startup and added to the Device as a Dictionary of Settings. Both Settings and Device implement INotifyPropertyChanged and when a Setting's value changes, the Device raises the property Changed event for that Setting as well.
So when the program is started the values are shown like this, where the red arrow is the new custom user control and blue arrow points the working code directly in the view initial state:
If I change the value in the custom user control, its updated in the other one as well. Binding to source ok:
But if I change it on the other one, its not updated in the custom user control. Binding from source not ok:
Also, if I change the value of the str_pressureUnit condition, the conversion is only performed in the old code. Conversion condition binding not ok:
But the Enable/Disable Control, works correctly for both. Enable ok:
Simple examples with another custom user control and properties from Device that are not Dynamic work ok if I use the `Mode="TwoWay". I was thinking that maybe is a problem with the Multibindings or with the Dynamic Settings configuration, but as the one with EnableProperties work (which is a normal property of the ViewModel) I suspect it might be something related to the Dynamic properties.
This is how our Device class looks like:
public class Device : DynamicObject, INotifyPropertyChanged
{
#region Properties
...
public Dictionary<string, ISetting> DynamicSettings { get; private set; } = new Dictionary<string, ISetting>();
...
public Device(SettingDefinitionsProvider settingDefinitionsProvider, ICommunicationChannel communicationChannel, DeviceType deviceType)
{
...
foreach(ISetting s in DynamicSettings.Values)
{
s.PropertyChanged += OnSettingValueUpdated;
}
}
...
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName]string propertyname = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyname));
}
#endregion //INotifyPropertyChanged
#region EventHandlers
private void OnSettingValueUpdated(object sender, PropertyChangedEventArgs e)
{
if(sender is ISetting)
{
OnPropertyChanged((sender as ISetting).Name);
}
}
#endregion //EventHandlers
}
And the Setting class throws the PropertyChanged event when its value changes.
The viewModel has a SelectedDevice property and its set as DataContext of the view with the AutoWireViewModel functionality of Prism.
We are also using MultiBinding and some complex converters because some Settings shown value might change depending on other Setting Value, as well the control being enabled, etc. So one entry in the XAML for a Setting might get this big:
<TextBlock Grid.Row="4" Text="{Binding SelectedDevice.SafetyMargin.DisplayName}" Margin="5"/>
<TextBox Grid.Row="4" Grid.Column="1" Margin="5">
<TextBox.IsEnabled>
<MultiBinding Converter="{conv:EnableControlConverter}">
<Binding RelativeSource="{RelativeSource AncestorType={x:Type UserControl}, Mode=FindAncestor}" Path="DataContext.SelectedDevice.SafetyMargin"/>
<Binding RelativeSource="{RelativeSource AncestorType={x:Type UserControl}, Mode=FindAncestor}" Path="DataContext.EnableControls"/>
</MultiBinding>
</TextBox.IsEnabled>
<MultiBinding Converter="{conv:ConversionConverter}">
<Binding RelativeSource="{RelativeSource AncestorType={x:Type UserControl}, Mode=FindAncestor}" Path="DataContext.SelectedDevice.SafetyMargin"/>
<Binding RelativeSource="{RelativeSource AncestorType={x:Type UserControl}, Mode=FindAncestor}" Path="DataContext.SelectedDevice.PressureUnit"/>
</MultiBinding>
</TextBox>
<TextBlock Grid.Row="4" Grid.Column="2" Margin="5">
<TextBlock.Text>
<MultiBinding Converter="{conv:UnitLabelConverter}">
<Binding RelativeSource="{RelativeSource AncestorType={x:Type UserControl}, Mode=FindAncestor}" Path="DataContext.SelectedDevice.SafetyMargin"/>
<Binding RelativeSource="{RelativeSource AncestorType={x:Type UserControl}, Mode=FindAncestor}" Path="DataContext.SelectedDevice.PressureUnit"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
To simplify the addition of new Settings (this is the reason of the reading from xml and so on), I created a new user Control. Its XAML is:
<UserControl ...
x:Name="parent">
<Grid DataContext="{Binding ElementName=parent}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="{Binding Path=Setting.DisplayName}" Margin="5"/>
<TextBox Grid.Column="1" Margin="5">
<TextBox.IsEnabled>
<MultiBinding Converter="{conv:EnableControlConverter}">
<Binding Path="Setting"/>
<Binding Path="Enabled"/>
</MultiBinding>
</TextBox.IsEnabled>
<MultiBinding Mode="TwoWay" Converter="{conv:ConversionConverter}">
<Binding Path="Setting"/>
<Binding Path="ConditionSetting"/>
</MultiBinding>
</TextBox>
<TextBlock Grid.Column="2" Margin="5">
<TextBlock.Text>
<MultiBinding Converter="{conv:UnitLabelConverter}">
<Binding Path="Setting"/>
<Binding Path="ConditionSetting"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
And the code behind:
public partial class CustomTextBox : UserControl
{
protected static Logger logger = LogManager.GetCurrentClassLogger();
public static readonly DependencyProperty SettingProperty =
DependencyProperty.Register("Setting", typeof(object),
typeof(CustomTextBox), new PropertyMetadata(""));
public static readonly DependencyProperty ConditionSettingProperty =
DependencyProperty.Register("ConditionSetting", typeof(object),
typeof(CustomTextBox), new PropertyMetadata(""));
public static readonly DependencyProperty EnabledProperty =
DependencyProperty.Register("Enabled", typeof(object),
typeof(CustomTextBox), new PropertyMetadata(""));
public object Setting
{
get { return (object)GetValue(SettingProperty); }
set { SetValue(SettingProperty, value); }
}
public object ConditionSetting
{
get { return (object)GetValue(ConditionSettingProperty); }
set { SetValue(ConditionSettingProperty, value); }
}
public object Enabled
{
get { return (object)GetValue(EnabledProperty); }
set { SetValue(EnabledProperty, value); }
}
public CustomTextBox()
{
InitializeComponent();
}
}
And this is the new code in the view:
<controls:CustomTextBox Grid.Row="3"
Setting="{Binding SelectedDevice.SafetyMargin, Mode=TwoWay}"
ConditionSetting="{Binding SelectedDevice.PressureUnit, Mode=TwoWay}"
Enabled="{Binding EnableControls}"/>
Any advice will be highly appreciated, thanks in advance.
Did you debug ?
Is the callback OnSettingValueUpdated in your Device-Class called
in both cases and are there any differences ?
Are the setters of ConditionSetting/Setting in your UserControl
called ?
I would say that somehow the PropertyChanged is not executed properly or doesn't reach the Property ...
The problem was using references as values for the DependencyProperties. All the structure is working and the DependencyProperty hears the NotifyPropertyChanged event for the Setting, but it checks the equality before updating the value and then it finds that the reference is the same, not updating it therefore.
Coercion doesn't work because it's done before the check for equality.
The solutions we tried were:
Creating a new Setting every time the Setting.Value changes. This works but we were afraid of possible problems of references to old Settings not being updated somewhere in the code that wasn't listening to the PropertyChanged events, for example.
Adding additional Bindings to the Converters multibinding in the xaml of the UserControl, especifically the Setting.Value. There is also a PropertyChangedEvent for this property so when it changes, the Converter reevaluates and the UserControl works as we expected. We didn't have to change the actual code besides the XAML so we finally went with this solution.
Related
Im having a hard time with the following error:
I have a listview that is binded to an observable collection.
Lets say it looks like this:
XAMl:
<ListView ItemsSource="{Binding myCollection}" SelectedItem="{Binding selectedItem}">
ViewModel:
private Field selecteditem;
public Field selectedItem {
get { return selecteditem; }
set
{
selecteditem = value;
}
... //other code parts
myCollection = customClass.fillCollection(selectedLightColor, selectedDarkColor);
When i click on an item it is selected. When i click on another that is the selected one. This is totally okay. However at a certain point i need to recreate the whole observable collection that is connected to this listview.
If i didnt select anything it recreates the collection perfectly.
But, when i have a selected item it throws a System.NullReferenceException error to the property that is binded to the SelectedItem of the listview.
For the recreation im using the same code mentioned above (myCollection = customClass...)
I cant find a solution that solves the problem.
I have tried myCollection.Clear() and also selectedItem = null, but the error remained the same.
Im glad to hear any help!
I tried to reproduce the problem you describe, but it didn't work for me.
My example:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Core2022.SO.freasy
{
public class Field
{
public int Number { get; } = random.Next();
private static readonly Random random = new Random();
public static IEnumerable<Field> GetRandomFields()
=> Enumerable.Range(0, random.Next(10, 20)).Select(_ => new Field()).ToList().AsReadOnly();
}
}
using Simplified;
using System.Collections.Generic;
namespace Core2022.SO.freasy
{
public class FieldsViewModel : BaseInpc
{
private IEnumerable<Field> _fields = Field.GetRandomFields();
private RelayCommand? _refreshFields;
public IEnumerable<Field> Fields { get => _fields; set => Set(ref _fields, value); }
public RelayCommand RefreshFields => _refreshFields
??= new RelayCommand(_ => Fields = Field.GetRandomFields());
private Field? _selectedField;
public Field? SelectedField
{
get => _selectedField;
set => Set(ref _selectedField, value);
}
}
}
<Window x:Class="Core2022.SO.freasy.FieldsWindow"
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:Core2022.SO.freasy" xmlns:sys="clr-namespace:System;assembly=netstandard"
mc:Ignorable="d"
Title="FieldsWindow" Height="450" Width="800">
<Window.DataContext>
<local:FieldsViewModel/>
</Window.DataContext>
<Window.Resources>
<sys:String x:Key="null">No Selected Field</sys:String>
</Window.Resources>
<UniformGrid Columns="2">
<ListBox x:Name="listBox" ItemsSource="{Binding Fields}"
DisplayMemberPath="Number"
SelectedItem="{Binding SelectedField}"
VerticalAlignment="Center" HorizontalAlignment="Center"/>
<UniformGrid Columns="1">
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock.Text>
<PriorityBinding>
<Binding Path="SelectedField.Number" Mode="OneWay"/>
<Binding Source="{StaticResource null}"/>
</PriorityBinding>
</TextBlock.Text>
</TextBlock>
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock.Text>
<PriorityBinding>
<Binding Path="SelectedItem.Number" Mode="OneWay" ElementName="listBox"/>
<Binding Source="{StaticResource null}"/>
</PriorityBinding>
</TextBlock.Text>
</TextBlock>
<Button Content="Refresh Collection" Command="{Binding RefreshFields}"
VerticalAlignment="Center" HorizontalAlignment="Center" Padding="15 5"/>
</UniformGrid>
</UniformGrid>
</Window>
Perhaps you missed an important detail of your implementation in the explanations, which is the cause of the error you described.
Try changing my simple example to reproduce your problem.
BaseInpc and RelayCommand classes.
Source Code Archive: freasy.7z
Because the property setter called a method which used my private variable (selecteditem) the program couldnt handle the selecteditem variable to be null!
I can finally recreate my collection by adding a statement to the setter:
private Field selecteditem;
public Field selectedItem {
get { return selecteditem; }
set
{
selecteditem = value;
if (selecteditem != null)
{
//call method and do stuffs
}
}
i need to create an instance of a class with two property that would be used as converter parameter of a binding.
the class is as below:
public class UnitQuantityBindClass:DependencyObject
{
public static readonly DependencyProperty QuantityProperty = DependencyProperty.Register(
"Quantity", typeof(EQuantities), typeof(UnitQuantityBindClass));
public EQuantities Quantity
{
get
{
return (EQuantities) GetValue(QuantityProperty);
}
set { SetValue(QuantityProperty, value); }
}
public static readonly DependencyProperty UnitProperty = DependencyProperty.Register(
"Unit", typeof(Enum), typeof(UnitQuantityBindClass));
public Enum Unit
{
get { return (Enum)GetValue(UnitProperty); }
set { SetValue(UnitProperty, value); }
}
}
the xaml code is as below also:
<textboxunitconvertor:TextBoxUnitConvertor Name="gasDensityValueControl" InstantaneousConvert="True" Margin="96,163,0,0" IsEnabled="{Binding ElementName=chkGas,Path=IsChecked}" QuantityBind="{Binding _FluidBlackOilClass.SGGas_SC.Quantity , RelativeSource={RelativeSource AncestorType=Window}, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" Width="206" Height="28" HorizontalAlignment="Left" VerticalAlignment="Top">
<textboxunitconvertor:TextBoxUnitConvertor.TextBoxText>
<Binding Path="_FluidBlackOilClass.SGGas_SC.Value" RelativeSource="{RelativeSource AncestorType=Window}" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay" Converter="{StaticResource ValueStorageForUnitConverter}">
<Binding.ConverterParameter>
<classes:UnitQuantityBindClass Quantity="{Binding ElementName=gasDensityValueControl,Converter={StaticResource DummyConverter} ,Path=_Quantity,UpdateSourceTrigger=PropertyChanged, Mode=TwoWay, PresentationTraceSources.TraceLevel=High}" Unit="{Binding ElementName=gasDensityValueControl,Path=_CurrentUnitEnum,UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"></classes:UnitQuantityBindClass>
</Binding.ConverterParameter>
</Binding>
</textboxunitconvertor:TextBoxUnitConvertor.TextBoxText>
<textboxunitconvertor:TextBoxUnitConvertor.CurrentUnitEnumBind>
<Binding Source="{StaticResource CurrentFlowProWorkingClass}" Path="currentFlowProWorkingClass.ProjectUnitSystem" UpdateSourceTrigger="PropertyChanged" Mode="OneTime">
<Binding.ConverterParameter>
<classes:UnitQuantityBindClass Quantity="{Binding ElementName=gasDensityValueControl,Path=_Quantity,UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" Unit="{Binding ElementName=gasDensityValueControl,Path=_CurrentUnitEnum,UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"></classes:UnitQuantityBindClass>
</Binding.ConverterParameter>
</Binding>
</textboxunitconvertor:TextBoxUnitConvertor.CurrentUnitEnumBind>
</textboxunitconvertor:TextBoxUnitConvertor>
but the create class is empty and the Unit and quantity properties does not bind.
why?
Background: I'm new to WPF and have been trying to teach myself for a couple weeks now. I have an app that runs in a NavigationWindow with a few pages. The page in question has 5 textboxes, 4 of which are backed with dependency properties. Three of them are set up with ValidationRules for times, the fourth has ValidationRules for type double. The fifth textbox is the output of the calculation made from a button click event. The button is bound to a MultiDataTrigger, which enables the button when there are no validation errors. Buddy says "hey you have everything bound already, why not update the output box on binding so you don't have to click a button?".
This seems like a good idea and a nice weapon to put in my wpf toolbox. The button serves two purposes, to calculate the time for the output textbox, and to offer to navigate to another page with the current values. If I could show the result of the calculation in the textbox with a binding, I would just use the button to navigate to the next page. I've tried setting up an ObjectDataProvider to use with the fifth textbox so I can call a method to populate the result with a binding. So far I've only succeeded in causing numerous errors, including causing a stackoverflow on the page call to InitializeComponent();
public static readonly DependencyProperty timeBoxProperty =
DependencyProperty.Register("timeBox", typeof(string),
typeof(TtlPage), new UIPropertyMetadata("07:30"));
public static readonly DependencyProperty timeBoxProperty2 =
DependencyProperty.Register("timeBox2", typeof(string),
typeof(TtlPage), new UIPropertyMetadata("13:00"));
public static readonly DependencyProperty timeBoxProperty3 =
DependencyProperty.Register("timeBox3", typeof(string),
typeof(TtlPage), new UIPropertyMetadata("13:40"));
public static readonly DependencyProperty hoursBoxProperty =
DependencyProperty.Register("hoursBox", typeof(string),
typeof(TtlPage), new UIPropertyMetadata("9.00"));
public string timeBox
{
get { return (string)GetValue(timeBoxProperty); }
set { SetValue(timeBoxProperty, value); }
}
public string timeBox2
{
get { return (string)GetValue(timeBoxProperty2); }
set { SetValue(timeBoxProperty2, value); }
}
public string timeBox3
{
get { return (string)GetValue(timeBoxProperty3); }
set { SetValue(timeBoxProperty3, value); }
}
public string hoursBox
{
get { return (string)GetValue(hoursBoxProperty); }
set { SetValue(hoursBoxProperty, value); }
}
Part of button click, given the above, should I be accessing the textbox.text like below using the Textbox.Name property, or should I be grabbing it from the property or DependencyProperty above?:
private void Button_Click(object sender, RoutedEventArgs e)
{
DateTime inTime = DateTime.Parse(ttlInTime.Text);
DateTime outLunch = DateTime.Parse(ttlOutLunch.Text);
DateTime inLunch = DateTime.Parse(ttlInLunch.Text);
decimal hours = decimal.Parse(ttlHours.Text);
//etc.
}
The method for the ObjectDataProvider:
public string UpdateOutput()
{
//do stuff
}
Some XAML ObjectDataProvider, one of the input textboxes, and the output textbox:
<ObjectDataProvider x:Key="outputBox" ObjectType="{x:Type sys:String}" MethodName="UpdateOutput"/>
<Style x:Key="timeBox3" TargetType="TextBox" BasedOn="{StaticResource tbStyle}">
<Setter Property="Text">
<Setter.Value>
<Binding ElementName="This" Path="timeBox3" UpdateSourceTrigger="
<Binding.ValidationRules>
<local:TimeValidation/>
</Binding.ValidationRules>
</Binding>
</Setter.Value>
</Setter>
</Style>
<TextBox Name="ttlInLunch" Style="{StaticResource timeBox3}" Grid.Row="2" Grid.Column="1" TextChanged="TimeBox_TextChanged"
GotFocus="TimeBox_GotFocus"/>
<TextBox Margin="0,2,2,1" Name="ttlOutput" Grid.Row="4" Grid.Column="1" IsReadOnly="True" Background="Transparent" IsTabStop="False"
Text="{Binding Source={StaticResource outputBox}}"/>
So, I've been here http://msdn.microsoft.com/en-us/library/aa348824(v=vs.110).aspx, and worked with the example, and after a while, realized that the ObjectType wasn't supposed to be the return type of the method. It was actually just the name of the containing class, so I used ttlPage as the type (which is the page itself ttlPage : Page), and caused a stack overflow. I've done a ton of Googling and haven't come up with anything helpful. I haven't created any sort of converter for it, because the method returns a string, which I would assume is suitable for the textbox.text property. I've set a breakpoint in the UpdateOutput method, and have found that it doesn't even get called. How do I call the UpdateOutput method and have it's result bound to the output textbox while the user is typing? As far as when I calculate, I was just going to return from the method until there are no validation errors, at which point I would perform my calculations and return the calculated value ToString();
Try changing the access modifier to public for your method UpdateOutput. Currently it's a private method, so can't be executed by the framework.
public string UpdateOutput()
{
//do stuff
}
Bartosz was correct, I needed to define another class to hold my UpdateOutput method. There were also several other factors which contributed to the frustration. First I created a class to hold the method. I then found out the hard way that I forgot a default constructor on said class. Additionally I found I was not able to use DependencyProperties as parameters for the ObjectDataProvider. I removed the entire set of DependencyProperties and their respective bindings. The styles referencing these were also removed, as were the bindings to the validation class.
//the containing class
public partial class AutoFillBox
{
public AutoFillBox()
{
//dont forget a default constructor
}
public string UpdateOutput(string time1, string time2, string time3, string time4)
{
//do stuff
}
}
The ObjectDataProvider:
<ObjectDataProvider ObjectType="{x:Type local:AutoFillBox}" MethodName="UpdateOutput" x:Key="odpOutput">
<ObjectDataProvider.MethodParameters>
<sys:String>08:00</sys:String>
<sys:String>12:00</sys:String>
<sys:String>13:00</sys:String>
<sys:String>18:00</sys:String>
</ObjectDataProvider.MethodParameters>
Then it was simply bind the appropriate textboxes to the MethodParameters:
<TextBox Name="recIn" Style="{StaticResource tbStyle}" Grid.Row="1" Grid.Column="1"
TextChanged="TimeBox_TextChanged" GotFocus="TimeBox_GotFocus">
<TextBox.Text>
<Binding Source="{StaticResource odpOutput}" Path="MethodParameters[0]" BindsDirectlyToSource="True" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:TimeValidation/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
And bind the output of the method to a textbox control:
<TextBox Margin="0,2,2,1" Name="recOutput" Grid.Row="5" Grid.Column="1" IsReadOnly="True" Background="Transparent" IsTabStop="False"
Text="{Binding Source={StaticResource odpOutput}, Mode=OneWay}"/>
Here’s a (somewhat) pared-down example of some code I have that’s attempting to make use of a BindingGroup:
XAML:
<Window x:Class="BindingGroupQuandary.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:BindingGroupQuandary"
Title="Binding Group Test" Width="400" Height="400" MinWidth="400" MinHeight="400">
<Grid Margin="5">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<GroupBox Header="Items">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ListView Name="dataList">
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn Header="First Property" DisplayMemberBinding="{Binding FirstProperty}" />
<GridViewColumn Header="Second Property" DisplayMemberBinding="{Binding SecondProperty}" />
</GridView.Columns>
</GridView>
</ListView.View>
</ListView>
<Button Grid.Row="1" Content="Add Item" Click="add_Click" HorizontalAlignment="Left" Margin="0,5,0,5" />
</Grid>
</GroupBox>
<GroupBox Grid.Row="1" Name="editControls" Header="Edit Item">
<GroupBox.BindingGroup>
<BindingGroup Name="editItemBindings"/>
</GroupBox.BindingGroup>
<StackPanel>
<Label Content="First Property (Required)"/>
<TextBox>
<TextBox.Text>
<Binding Path="FirstProperty">
<Binding.ValidationRules>
<local:NotEmptyValidationRule ValidationStep="RawProposedValue"/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Label Content="Second Property (Required)"/>
<TextBox>
<TextBox.Text>
<Binding Path="SecondProperty">
<Binding.ValidationRules>
<local:NotEmptyValidationRule ValidationStep="RawProposedValue"/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Button Content="Commit" Click="commit_Click" HorizontalAlignment="Left" Margin="0,10,0,0"/>
</StackPanel>
</GroupBox>
</Grid>
</Window>
Code-behind:
using System.Collections.ObjectModel;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
namespace BindingGroupQuandary
{
class SomeData
{
public string FirstProperty { get; set; }
public string SecondProperty { get; set; }
}
class NotEmptyValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
string textValue = (value as string);
ValidationResult result;
if (string.IsNullOrWhiteSpace(textValue))
{
result = new ValidationResult(false, "The field must contain a text value and cannot be empty.");
}
else
{
result = new ValidationResult(true, null);
}
return result;
}
}
public partial class MainWindow : Window
{
private ObservableCollection<SomeData> _items = new ObservableCollection<SomeData>();
public MainWindow()
{
InitializeComponent();
dataList.ItemsSource = _items;
}
private void add_Click(object sender, RoutedEventArgs e)
{
editControls.DataContext = (new SomeData() { FirstProperty = "This property has an initial value." });
editControls.BindingGroup.BeginEdit();
}
private void commit_Click(object sender, RoutedEventArgs e)
{
SomeData current = editControls.DataContext as SomeData;
if (current != null)
{
if (editControls.BindingGroup.CommitEdit())
{
_items.Add(current);
editControls.DataContext = null;
}
}
}
}
}
The effect I’m looking for is that:
I click “Add Item” to bind an object to the editing controls in the lower half of the window (which normally aren’t visible until the data context is supplied in the actual application).
I intentionally don’t set ValidatesOnTargetUpdated on either property binding because I don’t necessarily want to show any error adorners yet; the field text already hints that the fields are required.
When I do click Commit, I want the BindingGroup to re-check all of the validation rules to catch any fields that are still empty and show the error adorners at that point.
Unfortunately, using the sample code above, if I click “Add” and then click “Commit” immediately without filling in the “Second Property” field or changing anything, the binding group doesn’t call my validation rules at all; CommitEdit returns “true” and the object is passed through to the underlying collection. Calling ValidateWithoutUpdate also just returns “true.” My tests with the application seem to suggest that CommitEdit and ValidateWithoutUpdate actually only call validation rules where:
The rule is currently false/invalid.
The rule is currently valid but the value has changed.
The methods don’t seem to reevaluate valid rules for which the value hasn’t changed. I suppose this behavior makes sense (and perhaps this is just a general binding behavior?), but it seems to contradict the documentation which states, for CommitEdit:
Runs all the ValidationRule objects and updates the binding sources if all validation rules succeed.
I imagine the validation rules mentioned in that statement could refer only to the top-level rules in the BindingGroup itself and not the validation rules in the individual bindings. At any rate, my actual question is: is it possible to get the BindingGroup to re-check all of the validation rules in this example; if not, could I do that directly by using the BindingExpressions collection? Or, should I just give up and rely on ValidatesOnTargetUpdated (perhaps paired with a less glaring adorner template) to set the initial rule states correctly when the data context is established?
I ran in to the unique situation today where I needed to bind the Visible property of a button in a DataGridRow to be based on both a property of the bound object and of the model backing it.
XAML:
<t:DataGrid ItemsSource="{Binding Items}">
<t:DataGrid.Columns>
<t:DataGridTemplateColumn>
<t:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Visibility="IsEditable OR IsAdmin"/>
</DataTemplate>
</t:DataGridTemplateColumn.CellTemplate>
</t:DataGridTemplateColumn>
</t:DataGrid.Columns>
</t:DataGrid>
Model:
class TheModel
{
public ObservableCollection<Whatever> Items { get; set; }
public bool IsAdmin { get; set; }
}
Class:
class Whatever
{
public bool IsEditable { get; set; }
}
This stumped me. The only concept that I could think might work would be somehow passing the bound object and either the entire model or just the IsAdmin property to a static method on a converter or something. Any ideas?
Firstly, you cannot directly use a Boolean for Visibility. You need to use the BooleanToVisibilityConverter.
Secondly, about the OR, you have different options:
Create a readonly property IsEditableOrAdmin in Whatever which returns the value you want. Drawback: Your Whatever will need a back-reference to TheModel.
Use a MultiBinding and write an IMultiValueConverter. Then, pass both values in the MultiBinding. Since TheModel is no longer in the DataContext scope at that point, you could use the ElementName property of the Binding to refer to a UI element where TheModel is still accessible.
Example (untested):
<SomeElementOutsideYourDataGrid Tag="{Binding TheModel}" />
...
<Button>
<Button.Visibility>
<MultiBinding Converter="{StaticResource yourMultiValueConverter}">
<Binding Path="IsEditable" />
<Binding ElementName="SomeElementOutsideYourDataGrid" Path="Tag.IsAdmin"/>
</MultiBinding>
</Button.Visibility>
</Button>
Use a more powerful binding framework such as PyBinding.