sources/Google.Solutions.Mvvm/Binding/ToolStripMenuBindingExtensions.cs (212 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 Google.Solutions.Common.Linq; using Google.Solutions.Common.Runtime; using System; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.Drawing; using System.Linq; using System.Linq.Expressions; using System.Windows.Forms; namespace Google.Solutions.Mvvm.Binding { public static class ToolStripMenuBindingExtensions { public static void BindItem<TModel>( this ToolStripItem item, TModel model, Func<TModel, bool> isSeparator, Expression<Func<TModel, string?>> getText, Expression<Func<TModel, string?>> getToolTip, Expression<Func<TModel, Image?>> getImage, Expression<Func<TModel, Keys>> getShortcuts, Expression<Func<TModel, bool>> isVisible, Expression<Func<TModel, bool>> isEnabled, Expression<Func<TModel, ToolStripItemDisplayStyle>> getStyle, Func<TModel, ObservableCollection<TModel>> getChildren, Action<TModel> click, IBindingContext bindingContext) where TModel : class, INotifyPropertyChanged { item.BindReadonlyProperty( c => c.Text, model, getText, bindingContext); item.BindReadonlyProperty( c => c.ToolTipText, model, getToolTip, bindingContext); item.BindReadonlyProperty( c => c.Image, model, getImage, bindingContext); item.BindReadonlyProperty( c => c.Visible, model, isVisible, bindingContext); item.BindReadonlyProperty( c => c.Enabled, model, isEnabled, bindingContext); item.BindReadonlyProperty( c => c.DisplayStyle, model, getStyle, bindingContext); void OnClick(object sender, EventArgs args) => click(model); item.Click += OnClick; bindingContext.OnBindingCreated( item, Disposable.Create(() => item.Click -= OnClick)); if (item is ToolStripMenuItem toolStripMenuItem) { toolStripMenuItem.BindReadonlyProperty( c => c.ShortcutKeys, model, getShortcuts, bindingContext); var subCommands = getChildren(model); if (subCommands != null) { toolStripMenuItem.DropDownItems.BindCollection( subCommands, isSeparator, getText, getToolTip, getImage, getShortcuts, isVisible, isEnabled, getStyle, getChildren, click, bindingContext); } } } public static void BindCollection<TModel>( this ToolStripItemCollection view, ObservableCollection<TModel> modelCollection, Func<TModel, bool> isSeparator, Expression<Func<TModel, string?>> getText, Expression<Func<TModel, string?>> getToolTip, Expression<Func<TModel, Image?>> getImage, Expression<Func<TModel, Keys>> getShortcuts, Expression<Func<TModel, bool>> isVisible, Expression<Func<TModel, bool>> isEnabled, Expression<Func<TModel, ToolStripItemDisplayStyle>> getStyle, Func<TModel, ObservableCollection<TModel>> getChildren, Action<TModel> click, IBindingContext bindingContext) where TModel : class, INotifyPropertyChanged { ToolStripItem CreateMenuItem(TModel model) { if (isSeparator(model)) { return new ToolStripSeparator() { Tag = model }; } else { // // NB. If the display style is Image, then this must be // a toolbar. For toolbars, using ToolStripButton ensures // that the button and its hottracking rectangle is sized // correctly. // var item = getStyle.Compile()(model) == ToolStripItemDisplayStyle.Image ? (ToolStripItem)new ToolStripButton() : (ToolStripItem)new ToolStripMenuItem(); item.Tag = model; item.BindItem( model, isSeparator, getText, getToolTip, getImage, getShortcuts, isVisible, isEnabled, getStyle, getChildren, click, bindingContext); return item; } } // // Do initial population. // view.AddRange(modelCollection .Select(modelItem => CreateMenuItem(modelItem)) .ToArray()); // // Propagate changes. // var binding = new Binding<TModel>( view, modelCollection, CreateMenuItem); // // NB. ToolStripItemCollection aren't componnets, so we // cannot report this binding to the binding context. // } private sealed class Binding<TModel> : IDisposable { private readonly ToolStripItemCollection view; private readonly ObservableCollection<TModel> model; private readonly Func<TModel, ToolStripItem> createMenuItem; public Binding( ToolStripItemCollection view, ObservableCollection<TModel> model, Func<TModel, ToolStripItem> createMenuItem) { this.view = view; this.model = model; this.createMenuItem = createMenuItem; this.model.CollectionChanged += Model_CollectionChanged; } private void Model_CollectionChanged( object sender, NotifyCollectionChangedEventArgs e) { var newModelItems = e.NewItems?.OfType<TModel>(); var oldModelItems = e.OldItems?.OfType<TModel>(); switch (e.Action) { case NotifyCollectionChangedAction.Add: { var index = e.NewStartingIndex; foreach (var newModelItem in newModelItems.EnsureNotNull()) { this.view.Insert( index++, this.createMenuItem(newModelItem)); } } break; case NotifyCollectionChangedAction.Remove: { foreach (var oldViewItem in this.view .OfType<ToolStripMenuItem>() .Where(item => item.Tag is TModel) .Where(item => oldModelItems.Contains((TModel)item.Tag)) .ToList()) { this.view.Remove(oldViewItem); } } break; case NotifyCollectionChangedAction.Reset: { this.view.Clear(); } break; case NotifyCollectionChangedAction.Replace: case NotifyCollectionChangedAction.Move: throw new NotImplementedException(); } } public void Dispose() { this.model.CollectionChanged -= Model_CollectionChanged; } } } }