sources/Google.Solutions.Mvvm/Binding/Commands/CommandContainer.cs (288 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.Util;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
namespace Google.Solutions.Mvvm.Binding.Commands
{
/// <summary>
/// Set of commands.
/// </summary>
/// <typeparam name="TContext"></typeparam>
public interface ICommandContainer<TContext> : IDisposable
where TContext : class
{
ICommandContainer<TContext> AddCommand(
IContextCommand<TContext> command);
ICommandContainer<TContext> AddCommand(
IContextCommand<TContext> command,
int? index);
void AddSeparator(int? index = null);
void ExecuteCommandByKey(Keys keys);
void ExecuteDefaultCommand();
void BindTo(
ToolStripDropDownMenu menu,
IBindingContext bindingContext);
void BindTo(
ToolStripMenuItem menu,
IBindingContext bindingContext);
}
public sealed class CommandContainer<TContext> : ICommandContainer<TContext>, IDisposable
where TContext : class
{
private readonly ToolStripItemDisplayStyle displayStyle;
private readonly ObservableCollection<MenuItemViewModelBase> menuItems;
private readonly IBindingContext bindingContext;
internal IContextSource<TContext> ContextSource { get; }
internal ObservableCollection<MenuItemViewModelBase> MenuItems => this.menuItems;
private CommandContainer(
ToolStripItemDisplayStyle displayStyle,
IContextSource<TContext> contextSource,
ObservableCollection<MenuItemViewModelBase> items,
IBindingContext bindingContext)
{
this.displayStyle = displayStyle;
this.menuItems = items;
this.ContextSource = contextSource;
this.bindingContext = bindingContext;
}
public CommandContainer(
ToolStripItemDisplayStyle displayStyle,
IContextSource<TContext> contextSource,
IBindingContext bindingContext)
: this(
displayStyle,
contextSource,
new ObservableCollection<MenuItemViewModelBase>(),
bindingContext)
{
if (this.ContextSource is INotifyPropertyChanged observable)
{
observable.OnPropertyChange(
s => ((IContextSource<TContext>)s).Context,
context =>
{
MenuItemViewModel.OnContextUpdated(this.menuItems);
},
bindingContext);
}
}
public void BindTo(
ToolStripItemCollection view,
IBindingContext bindingContext)
{
view.BindCollection(
this.menuItems,
m => m is SeparatorViewModel,
m => m.Text,
m => m.ToolTip,
m => m.Image,
m => m.ShortcutKeys,
m => m.IsVisible,
m => m.IsEnabled,
m => m.DisplayStyle,
m => m.Children,
m => m.Invoke(),
bindingContext);
}
public void ForceRefresh()
{
MenuItemViewModel.OnContextUpdated(this.menuItems);
}
//---------------------------------------------------------------------
// IDisposable.
//---------------------------------------------------------------------
public void Dispose()
{
}
//---------------------------------------------------------------------
// ICommandContainer.
//---------------------------------------------------------------------
public void BindTo(
ToolStripDropDownMenu menu,
IBindingContext bindingContext)
{
BindTo(menu.Items, bindingContext);
if (!(this.ContextSource is INotifyPropertyChanged))
{
//
// The source isn't observable. Perform an explicit
// refresh every time the menu is opened.
//
menu.Opening += (sender, args) => ForceRefresh();
}
}
public void BindTo(
ToolStripMenuItem menu,
IBindingContext bindingContext)
{
BindTo(menu.DropDownItems, bindingContext);
if (!(this.ContextSource is INotifyPropertyChanged))
{
//
// The source isn't observable. Perform an explicit
// refresh every time the menu is opened.
//
menu.DropDownOpening += (sender, args) => ForceRefresh();
}
}
public ICommandContainer<TContext> AddCommand(IContextCommand<TContext> command)
=> AddCommand(command, null);
public ICommandContainer<TContext> AddCommand(IContextCommand<TContext> command, int? index)
{
var item = new MenuItemViewModel(
this.displayStyle,
command,
this);
if (index != null)
{
this.menuItems.Insert(Math.Min(index.Value, this.menuItems.Count), item);
}
else
{
this.menuItems.Add(item);
}
//
// Set initial state using current context.
//
item.OnContextUpdated();
return new CommandContainer<TContext>(
this.displayStyle,
this.ContextSource,
item.Children,
this.bindingContext);
}
public void AddSeparator(int? index = null)
{
var item = new SeparatorViewModel();
if (index != null)
{
this.menuItems.Insert(index.Value, item);
}
else
{
this.menuItems.Add(item);
}
}
public void ExecuteCommandByKey(Keys keys)
{
this.menuItems
.Where(i => i.ShortcutKeys == keys && i.IsVisible && i.IsEnabled)
.FirstOrDefault()?
.Invoke();
}
public void ExecuteDefaultCommand()
{
this.menuItems
.Where(i => i.IsDefault && i.IsVisible && i.IsEnabled)
.FirstOrDefault()?
.Invoke();
}
internal abstract class MenuItemViewModelBase : ViewModelBase
{
private bool isVisible;
private bool isEnabled;
public MenuItemViewModelBase(
ToolStripItemDisplayStyle displayStyle)
{
this.DisplayStyle = displayStyle;
this.Children = new ObservableCollection<MenuItemViewModelBase>();
}
public ToolStripItemDisplayStyle DisplayStyle { get; }
//-----------------------------------------------------------------
// Virtual properties.
//-----------------------------------------------------------------
public virtual string? Text => null;
public virtual string? ToolTip => null;
public virtual Image? Image => null;
public virtual Keys ShortcutKeys => Keys.None;
public virtual bool IsSeparator => false;
public virtual bool IsDefault => false;
//-----------------------------------------------------------------
// Mutable observable properties.
//-----------------------------------------------------------------
public ObservableCollection<MenuItemViewModelBase> Children { get; }
public bool IsVisible
{
get => this.isVisible;
set
{
this.isVisible = value;
RaisePropertyChange();
}
}
public bool IsEnabled
{
get => this.isEnabled;
set
{
this.isEnabled = value;
RaisePropertyChange();
}
}
//-----------------------------------------------------------------
// Actions.
//-----------------------------------------------------------------
public virtual void Invoke() { }
}
internal class SeparatorViewModel : MenuItemViewModelBase
{
public SeparatorViewModel()
: base(ToolStripItemDisplayStyle.None)
{
}
public override bool IsSeparator => true;
}
internal class MenuItemViewModel : MenuItemViewModelBase
{
private readonly IContextCommand<TContext> command;
private readonly CommandContainer<TContext> container;
public MenuItemViewModel(
ToolStripItemDisplayStyle displayStyle,
IContextCommand<TContext> command,
CommandContainer<TContext> container)
: base(displayStyle)
{
this.command = command;
this.container = container;
}
internal void OnContextUpdated()
{
var context = this.container.ContextSource.Context;
if (context == null)
{
//
// Assume all commands are unavailable. Hiding
// all menu items looks awkward though, so disable
// them.
//
this.IsVisible = true;
this.IsEnabled = false;
return;
}
switch (this.command.QueryState(context))
{
case CommandState.Disabled:
this.IsVisible = true;
this.IsEnabled = false;
break;
case CommandState.Enabled:
this.IsVisible = true;
this.IsEnabled = true;
break;
case CommandState.Unavailable:
this.IsVisible = false;
break;
}
OnContextUpdated(this.Children);
}
internal static void OnContextUpdated(
IEnumerable<MenuItemViewModelBase> items)
{
foreach (var item in items.OfType<MenuItemViewModel>())
{
item.OnContextUpdated();
}
}
//-----------------------------------------------------------------
// Read-only observable properties.
//-----------------------------------------------------------------
public override string? Text => this.command.Text;
public override string? ToolTip
=> this.DisplayStyle == ToolStripItemDisplayStyle.Image
? this.command.Text.Replace("&", "")
: null;
public override Image? Image => this.command.Image;
public override Keys ShortcutKeys => this.command.ShortcutKeys;
public override bool IsDefault => this.command.IsDefault;
//-----------------------------------------------------------------
// Actions.
//-----------------------------------------------------------------
public override async void Invoke()
{
try
{
var context = this.container.ContextSource.Context;
if (context == null)
{
//
// Context disappeared again, nevermind.
//
return;
}
await this.command
.ExecuteAsync(context)
.ConfigureAwait(true);
this.container.bindingContext.OnCommandExecuted(this.command);
}
catch (Exception e) when (e.IsCancellation())
{
// Ignore.
}
catch (Exception e)
{
this.container.bindingContext.OnCommandFailed(null, this.command, e);
}
}
}
}
}