I have a control (named GridView
in the example below) and a view model that are bound through 2-way binding on their SelectedValue
property. I want to forbid certain values for that property in the GridView
control through the use of a CoerceValueCallback
.
When the binding pushes that invalid value (42 in this example) into the DependencyProperty
, the CoerceValue
method discards that value and the control keeps its previous value as expected. Unfortunately, because the DependencyProperty
has not changed the binding does not update the source value in the view model.
How can I update the property in the view model with the value in the control when cancelling the property value change?
class ViewModel : INotifyPropertyChanged
{
public int? SelectedValue
{
get { return _selectedValue; }
set
{
_selectedValue = value;
RaisePropertyChanged("SelectedValue");
}
}
private int? _selectedValue;
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string name)
{
var propertyChanged = PropertyChanged;
if (propertyChanged != null)
{
propertyChanged(this, new PropertyChangedEventArgs(name));
}
}
}
class GridView : FrameworkElement
{
public object SelectedValue
{
get { return (object)GetValue(SelectedValueProperty); }
set { SetValue(SelectedValueProperty, value); }
}
public static readonly DependencyProperty SelectedValueProperty =
DependencyProperty.Register("SelectedValue", typeof(object), typeof(GridView),
new PropertyMetadata(null, null, new CoerceValueCallback(CoerceValue)));
private static object CoerceValue(DependencyObject d, object baseValue)
{
// forbid value 42 and return previous value
if (Equals(baseValue, 42))
{
return d.GetValue(GridView.SelectedValueProperty);
}
return baseValue;
}
}
[STAThread]
static void Main()
{
ViewModel vm = new ViewModel();
GridView grid = new GridView();
Binding binding = new Binding("SelectedValue") { Source = vm, Mode = BindingMode.TwoWay };
grid.SetBinding(GridView.SelectedValueProperty, binding);
vm.SelectedValue = 12;
Console.WriteLine("should be 12: {0} = {1}", grid.SelectedValue, vm.SelectedValue);
grid.SelectedValue = 23;
Console.WriteLine("should be 23: {0} = {1}", grid.SelectedValue, vm.SelectedValue);
vm.SelectedValue = 42;
Console.WriteLine("should still be 23: {0} = {1}", grid.SelectedValue, vm.SelectedValue);
}
I have tried
var binding = BindingOperations.GetBindingExpression(d,
GridView.SelectedValueProperty);
binding.UpdateSource();
in the CoerceValue
method to no avail. I even tried the previous 2 lines with Dispatcher.CurrentDispatcher.BeginInvoke(updateSource, DispatcherPriority.DataBind);
which was suggested in a similar stack overflow question.
Any other ideas?
Update:
@Berryl suggested I should do that logic in the view model and I agree with him... except that I can't. I will explain the full use case I tried to simplify in the code above.
GridView is not my control but a 3rd-party data grid from which I inherit. When a user edits a record, we pop up a window in which the user edits the record and then the view model refreshes the data and reselects the old record using its ID. Everything works well until the user uses the filtering capabilities of the grid. If the edit on the record would make the row hidden because of the filter, the following occurs:
ValueList
propertySelectedValue
property with a record that is in the ValueList
SelectedValue
but discards it because it is filtered outICommand
mapped to the double-click is called in the view modelSelectedValue
propertyI found this solution in my emails from way back and thought I should share it (with a little cleanup). This needed support calls to both Telerik and Microsoft at the time.
Basically, the reason I was not able to cancel a property update from the CoerceValueCallback is because:
Now the long answer on how to fix it:
static GridView()
{
SelectedItemProperty.OverrideMetadata(typeof(GridView), new FrameworkPropertyMetadata(null, CoerceSelectedItemProperty));
}
// Rewriting SelectedItem coercion logic because of the following issue (we had support calls to both Telerik and Microsoft)
// When
// - a filter is applied on the grid
// - user refreshes after modifying a record
// - the record he has changed would be filtered out by the the grid filter
// - the view model sets the SelectedItem to the modified record (which is hidden now)
//
// Telerik's coercion method resets the selected item to a visible row but because WPF's
// binding occurs before the coercion method, the view model is not updated.
// Even a call to UpdateSource() on the binding does not work in this case because the binding
// is already updating the target while this happens so it does nothing when you call it.
private static object CoerceSelectedItemProperty(DependencyObject d, object baseValue)
{
// Call normal Coercion method because we don't want to rewrite Telerik's logic
// and keep result to return it at the end.
object returnValue = SelectedItemProperty.GetMetadata(typeof(RadGridView)).CoerceValueCallback(d, baseValue);
var control = (GridView)d;
// If coerce returned something other than DependencyProperty.UnsetValue we can use it to push it back to
// the binding source because it is of the right type and the right value.
// The only case when we can use control.SelectedItem is when coerce returned UnsetValue otherwise the
// view model is always one click late.
object officialValue = returnValue == DependencyProperty.UnsetValue
? control.SelectedItem
: returnValue;
var binding = control.GetBindingExpression(SelectedItemProperty);
var source = binding.ResolvedSource;
var property = source.GetType().GetProperty(binding.ResolvedSourcePropertyName);
property.SetValue(source, officialValue, null);
return returnValue;
}