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"); } } } } }