wwauth/Google.Solutions.WWAuth/View/BindingExtensions.cs (218 lines of code) (raw):

// // Copyright 2022 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 System; using System.ComponentModel; using System.Diagnostics; using System.Linq.Expressions; using System.Reflection; namespace Google.Solutions.WWAuth.View { /// <summary> /// Extension methods to implement simple two-way data bindings /// between WinForms controls and model classes. /// /// (Source: IAP Desktop) /// </summary> public static class BindingExtensions { public static Binding OnPropertyChange<TObject, TProperty>( this TObject observed, Expression<Func<TObject, TProperty>> modelProperty, Action<TProperty> newValue) where TObject : INotifyPropertyChanged { 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"); } } public static Binding OnControlPropertyChange<TControl, TProperty>( this TControl observed, Expression<Func<TControl, TProperty>> controlProperty, Action<TProperty> newValue) where TControl : IComponent { 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"); } } public static void BindProperty<TControl, TProperty, TModel>( this TControl control, Expression<Func<TControl, TProperty>> controlProperty, TModel model, Expression<Func<TModel, TProperty>> modelProperty, IContainer container = null) where TModel : INotifyPropertyChanged where TControl : IComponent { // Apply initial value. var modelValue = modelProperty.Compile()(model); CreateSetter(control, controlProperty)(modelValue); var forwardBinding = control.OnControlPropertyChange( controlProperty, CreateSetter(model, modelProperty)); var reverseBinding = model.OnPropertyChange( modelProperty, CreateSetter(control, controlProperty)); // Wire up these two bindings so that we do not deliver // updates in cycles. forwardBinding.Peer = reverseBinding; reverseBinding.Peer = forwardBinding; if (container != null) { // To ensure that the bindings are disposed, add them to the // container of the control. container.Add(forwardBinding); container.Add(reverseBinding); } } public static void BindReadonlyProperty<TControl, TProperty, TModel>( this TControl control, Expression<Func<TControl, TProperty>> controlProperty, TModel model, Expression<Func<TModel, TProperty>> modelProperty, IContainer container = null) where TModel : INotifyPropertyChanged where TControl : IComponent { // Apply initial value. var modelValue = modelProperty.Compile()(model); CreateSetter(control, controlProperty)(modelValue); var binding = model.OnPropertyChange( modelProperty, CreateSetter(control, controlProperty)); if (container != null) { // To ensure that the bindings are disposed, add them to the // container of the control. container.Add(binding); } } public abstract class Binding : Component { public bool IsBusy { get; internal set; } = false; public Binding Peer { get; internal set; } } private sealed class EventHandlerBinding<TControl, TProperty> : Binding, IDisposable 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)); } protected override void Dispose(bool disposing) { if (disposing) { this.eventInfo.RemoveEventHandler( this.observed, new EventHandler(Observed_PropertyChanged)); } } } private sealed 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; } protected override void Dispose(bool disposing) { if (disposing) { this.observed.PropertyChanged -= Observed_PropertyChanged; } } } } }