sources/Google.Solutions.Mvvm/Binding/BindingExtensions.cs (339 lines of code) (raw):
//
// Copyright 2019 Google LLC
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//
using Google.Solutions.Common.Util;
using Google.Solutions.Mvvm.Controls;
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;
namespace Google.Solutions.Mvvm.Binding
{
/// <summary>
/// Extension methods to implement simple two-way data bindings
/// between WinForms controls and model classes.
/// </summary>
public static class BindingExtensions
{
internal static Binding CreatePropertyChangeBinding<TObject, TProperty>(
TObject observed,
Expression<Func<TObject, TProperty>> modelProperty,
Action<TProperty> newValue)
where TObject : class, INotifyPropertyChanged
{
Precondition.ExpectNotNull(observed, nameof(observed));
Precondition.ExpectNotNull(modelProperty, nameof(modelProperty));
Debug.Assert(modelProperty.NodeType == ExpressionType.Lambda);
if (modelProperty.Body is MemberExpression memberExpression &&
memberExpression.Member is PropertyInfo propertyInfo)
{
return new NotifyPropertyChangedBinding<TObject, TProperty>(
observed,
propertyInfo.Name,
modelProperty.Compile(),
newValue);
}
else
{
throw new ArgumentException("Expression does not resolve to a property");
}
}
internal static Binding CreateControlPropertyChangeBinding<TControl, TProperty>(
TControl observed,
Expression<Func<TControl, TProperty>> controlProperty,
Action<TProperty> newValue)
where TControl : class, IComponent
{
Precondition.ExpectNotNull(observed, nameof(observed));
Precondition.ExpectNotNull(controlProperty, nameof(controlProperty));
Debug.Assert(controlProperty.NodeType == ExpressionType.Lambda);
if (controlProperty.Body is MemberExpression memberExpression &&
memberExpression.Member is PropertyInfo propertyInfo)
{
// Look for a XxxChanged event.
var changedEvent = typeof(TControl).GetEvent(propertyInfo.Name + "Changed");
if (changedEvent == null)
{
throw new ArgumentException(
$"Cannot observe {propertyInfo.Name} because class does not " +
"provide an appropriate event");
}
return new EventHandlerBinding<TControl, TProperty>(
observed,
changedEvent,
controlProperty.Compile(),
newValue);
}
else
{
throw new ArgumentException("Expression does not resolve to a property");
}
}
private static Action<TProperty> CreateSetter<TObject, TProperty>(
TObject obj,
Expression<Func<TObject, TProperty>> controlProperty)
{
Debug.Assert(controlProperty.NodeType == ExpressionType.Lambda);
if (controlProperty.Body is MemberExpression memberExpression &&
memberExpression.Member is PropertyInfo propertyInfo)
{
return value => propertyInfo.SetValue(obj, value);
}
else
{
throw new ArgumentException("Expression does not resolve to a property");
}
}
//---------------------------------------------------------------------
// OnChange callbacks.
//---------------------------------------------------------------------
public static void OnControlPropertyChange<TControl, TProperty>(
this TControl observed,
Expression<Func<TControl, TProperty>> controlProperty,
Action<TProperty> newValue,
IBindingContext bindingContext)
where TControl : class, IComponent
{
Precondition.ExpectNotNull(bindingContext, nameof(bindingContext));
var binding = CreateControlPropertyChangeBinding(
observed,
controlProperty,
newValue);
bindingContext.OnBindingCreated(observed, binding);
}
public static void OnPropertyChange<TObject, TProperty>(
this TObject observed,
Expression<Func<TObject, TProperty>> modelProperty,
Action<TProperty> newValue,
IBindingContext bindingContext)
where TObject : class, INotifyPropertyChanged
{
Precondition.ExpectNotNull(bindingContext, nameof(bindingContext));
var binding = CreatePropertyChangeBinding(
observed,
modelProperty,
newValue);
if (binding is IComponent component)
{
bindingContext.OnBindingCreated(component, binding);
}
}
//---------------------------------------------------------------------
// Binding for bare properties.
//---------------------------------------------------------------------
public static void BindProperty<TControl, TProperty, TModel>(
this TControl control,
Expression<Func<TControl, TProperty>> controlProperty,
TModel model,
Expression<Func<TModel, TProperty>> modelProperty,
IBindingContext bindingContext)
where TModel : class, INotifyPropertyChanged
where TControl : class, IComponent
{
Precondition.ExpectNotNull(controlProperty, nameof(controlProperty));
Precondition.ExpectNotNull(model, nameof(model));
Precondition.ExpectNotNull(modelProperty, nameof(modelProperty));
Precondition.ExpectNotNull(bindingContext, nameof(bindingContext));
//
// Apply initial value.
//
var modelValue = modelProperty.Compile()(model);
CreateSetter(control, controlProperty)(modelValue);
var forwardBinding = CreateControlPropertyChangeBinding(
control,
controlProperty,
CreateSetter(model, modelProperty));
var reverseBinding = CreatePropertyChangeBinding(
model,
modelProperty,
CreateSetter(control, controlProperty));
//
// Wire up these two bindings so that we do not deliver
// updates in cycles.
//
forwardBinding.Peer = reverseBinding;
reverseBinding.Peer = forwardBinding;
control.AttachDisposable(forwardBinding);
control.AttachDisposable(reverseBinding);
bindingContext.OnBindingCreated(control, forwardBinding);
bindingContext.OnBindingCreated(control, reverseBinding);
}
public static void BindReadonlyProperty<TControl, TProperty, TModel>(
this TControl control,
Expression<Func<TControl, TProperty>> controlProperty,
TModel model,
Expression<Func<TModel, TProperty>> modelProperty,
IBindingContext bindingContext)
where TModel : class, INotifyPropertyChanged
where TControl : IComponent
{
Precondition.ExpectNotNull(controlProperty, nameof(controlProperty));
Precondition.ExpectNotNull(model, nameof(model));
Precondition.ExpectNotNull(modelProperty, nameof(modelProperty));
Precondition.ExpectNotNull(bindingContext, nameof(bindingContext));
//
// Apply initial value.
//
var modelValue = modelProperty.Compile()(model);
CreateSetter(control, controlProperty)(modelValue);
var binding = CreatePropertyChangeBinding(
model,
modelProperty,
CreateSetter(control, controlProperty));
control.AttachDisposable(binding);
bindingContext.OnBindingCreated(control, binding);
}
//---------------------------------------------------------------------
// Binding for ObservableProperties.
//---------------------------------------------------------------------
public static void BindObservableProperty<TControl, TProperty, TModel>(
this TControl control,
Expression<Func<TControl, TProperty>> controlProperty,
TModel model,
Expression<Func<TModel, IObservableProperty<TProperty>>> modelProperty,
IBindingContext bindingContext)
where TControl : class, IComponent
where TModel : class
{
Precondition.ExpectNotNull(controlProperty, nameof(controlProperty));
Precondition.ExpectNotNull(model, nameof(model));
Precondition.ExpectNotNull(modelProperty, nameof(modelProperty));
Precondition.ExpectNotNull(bindingContext, nameof(bindingContext));
//
// Apply initial value.
//
var observable = modelProperty.Compile()(model);
CreateSetter(control, controlProperty)(observable.Value);
Debug.Assert(observable is IObservableWritableProperty<TProperty>);
var forwardBinding = CreateControlPropertyChangeBinding(
control,
controlProperty,
val => ObservablePropertyHelper.SetValue(observable, val));
var reverseBinding = new NotifyObservablePropertyChangedBinding<TProperty>(
observable,
CreateSetter(control, controlProperty));
//
// Wire up these two bindings so that we do not deliver
// updates in cycles.
//
forwardBinding.Peer = reverseBinding;
reverseBinding.Peer = forwardBinding;
control.AttachDisposable(forwardBinding);
control.AttachDisposable(reverseBinding);
bindingContext.OnBindingCreated(control, forwardBinding);
bindingContext.OnBindingCreated(control, reverseBinding);
}
public static void BindReadonlyObservableProperty<TControl, TProperty, TModel>(
this TControl control,
Expression<Func<TControl, TProperty>> controlProperty,
TModel model,
Expression<Func<TModel, IObservableProperty<TProperty>>> modelProperty,
IBindingContext bindingContext)
where TControl : IComponent
where TModel : class
{
Precondition.ExpectNotNull(controlProperty, nameof(controlProperty));
Precondition.ExpectNotNull(model, nameof(model));
Precondition.ExpectNotNull(modelProperty, nameof(modelProperty));
Precondition.ExpectNotNull(bindingContext, nameof(bindingContext));
//
// Apply initial value.
//
var observable = modelProperty.Compile()(model);
CreateSetter(control, controlProperty)(observable.Value);
var binding = new NotifyObservablePropertyChangedBinding<TProperty>(
observable,
CreateSetter(control, controlProperty));
control.AttachDisposable(binding);
bindingContext.OnBindingCreated(control, binding);
}
//---------------------------------------------------------------------
// Inner classes.
//---------------------------------------------------------------------
public abstract class Binding : IDisposable
{
public bool IsBusy { get; internal set; } = false;
public Binding? Peer { get; internal set; }
public abstract void Dispose();
}
private sealed class EventHandlerBinding<TControl, TProperty> : Binding
where TControl : IComponent
{
private readonly TControl observed;
private readonly EventInfo eventInfo;
private readonly Func<TControl, TProperty> readPropertyFunc;
private readonly Action<TProperty> newValueAction;
private void Observed_PropertyChanged(object sender, EventArgs e)
{
if (this.Peer != null && this.Peer.IsBusy)
{
// Reentrant call - stop here to avoid changes bouncing
// back and forth.
return;
}
try
{
this.IsBusy = true;
this.newValueAction(this.readPropertyFunc(this.observed));
}
finally
{
this.IsBusy = false;
}
}
public EventHandlerBinding(
TControl observed,
EventInfo eventDescriptor,
Func<TControl, TProperty> readPropertyFunc,
Action<TProperty> newValueAction)
{
this.observed = observed;
this.eventInfo = eventDescriptor;
this.readPropertyFunc = readPropertyFunc;
this.newValueAction = newValueAction;
this.eventInfo.AddEventHandler(
this.observed,
new EventHandler(Observed_PropertyChanged));
}
public override void Dispose()
{
this.eventInfo.RemoveEventHandler(
this.observed,
new EventHandler(Observed_PropertyChanged));
}
}
internal class NotifyPropertyChangedBinding<TObject, TProperty> : Binding, IDisposable
where TObject : INotifyPropertyChanged
{
private readonly TObject observed;
private readonly string propertyName;
private readonly Func<TObject, TProperty> readPropertyFunc;
private readonly Action<TProperty> newValueAction;
private void Observed_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (this.Peer != null && this.Peer.IsBusy)
{
// Reentrant call - stop here to avoid changes bouncing
// back and forth.
return;
}
if (e.PropertyName == this.propertyName)
{
try
{
this.IsBusy = true;
this.newValueAction(this.readPropertyFunc(this.observed));
}
finally
{
this.IsBusy = false;
}
}
}
public NotifyPropertyChangedBinding(
TObject observed,
string propertyName,
Func<TObject, TProperty> readPropertyFunc,
Action<TProperty> newValueAction)
{
this.observed = observed;
this.propertyName = propertyName;
this.readPropertyFunc = readPropertyFunc;
this.newValueAction = newValueAction;
this.observed.PropertyChanged += Observed_PropertyChanged;
}
public override void Dispose()
{
this.observed.PropertyChanged -= Observed_PropertyChanged;
}
}
internal class NotifyObservablePropertyChangedBinding<TProperty>
: NotifyPropertyChangedBinding<IObservableProperty<TProperty>, TProperty>
{
public NotifyObservablePropertyChangedBinding(
IObservableProperty<TProperty> observed,
Action<TProperty> newValueAction)
: base(
observed,
"Value",
prop => prop.Value,
newValueAction)
{
}
}
private static class ObservablePropertyHelper
{
public static void SetValue<TProperty>(
IObservableProperty<TProperty> property,
TProperty newValue)
{
if (property is IObservableWritableProperty<TProperty> writable)
{
writable.Value = newValue;
}
else
{
throw new InvalidOperationException("Observable property is read-only");
}
}
}
}
}