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