Toolbar in a modular Application

Feb 25, 2011 at 8:26 AM

Hi all, I'm trying to write an extendable modular Application and have the need for a MainToolBar with dynamic content, dependent from the active Module. Now I have a solution for this, but I'm not quite sure if this is a "good" one.

First I created a ToolBarViewModel with a corresponding view. The ToolBarViewModel is instantiated as a Singleton and takes only a collection of ToolBarButtons.

    [Export(typeof(ToolBarViewModel))]
    [PartCreationPolicy(CreationPolicy.Shared)]
    class ToolBarViewModel
    {
        public ToolBarViewModel()
        {
            ToolBar = new BindableCollection<ToolBarButton>();
        }

        public BindableCollection<ToolBarButton> ToolBar { get; set; }
    }

The ToolBarView. In the ShellView is only a ContentControl for the visualization of the ToolBar.

    <ToolBarTray>
        <ToolBar x:Name="ToolBar">
            <ToolBar.ItemTemplate>
                <DataTemplate>
                    <Button Content="{Binding Name}" Command="{Binding Command}"></Button>
                </DataTemplate>
            </ToolBar.ItemTemplate>
        </ToolBar>
    </ToolBarTray>

In the ToolBarButton"ViewModel" are only two Properties, one for the Buttonname and one for the executed Command if the user clicks the button. For this I used the good old DelegateCommand.

    class ToolBarButton
    {
        public ToolBarButton(string name, Action<object> execute)
            : this(name, execute, null)
        {
        }

        public ToolBarButton(string name, Action<object> execute, Predicate<object> canExecute)
        {
            if (execute == null)
                throw new ArgumentNullException("execute");

            this.Name = name;

            Command = new DelegateCommand(execute, canExecute);
        }

        public string Name { get; set; }

        public DelegateCommand Command { get; set; }
    }

Now I have the possibility to add and remove the Buttons for the ToolBar and the corresponding actions in the modules ViewModels:

    class Module1ViewModel : Screen, IWorkspace
    {
        private ToolBarViewModel toolBar;

        [ImportingConstructor]
        public Module1ViewModel(ToolBarViewModel toolBar)
        {
            this.toolBar = toolBar;

            Module1Button = new ToolBarButton(
                "Module1",
                x => this.Module1ButtonExecute(),
                x => this.Module1ButtonCanExecute());
        }

        protected override void OnActivate()
        {
            base.OnActivate();

            toolBar.ToolBar.Add(Module1Button);
        }

        protected override void OnDeactivate(bool close)
        {
            base.OnDeactivate(close);

            toolBar.ToolBar.Remove(Module1Button);
        }

        public ToolBarButton Module1Button { get; set; }
        private void Module1ButtonExecute()
        {

        }

        private bool Module1ButtonCanExecute()
        {
            return true;
        }
    }
As all I can see it works fine for me, but maybe there is someone who has better approach for this?
Feb 25, 2011 at 10:35 AM

The only thing you could change is the fact that you have no way to customize toolbar items views.

If you are interested, you can check this thread where I provided a way to view-model-first approach to resolve items views within an ItemsControl.

Feb 28, 2011 at 8:45 AM

Thank you for your response. You're absolutely right. The solution above was only the first step and I hope I will achieve the customization with the help of your demo. Thanks for that.

 

Mar 29, 2011 at 11:40 AM

After a while doing other stuff I'm back with my ToolBar. First I wanted to get rid of the delegateCommand in my first post, so this is the way I do it now:

<UserControl x:Class="Brose.Test.CmToolBar.ToolBar.ToolBarView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:cal="http://www.caliburnproject.org"
             xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <ToolBarTray>
        <ToolBar x:Name="Items">
            <ToolBar.ItemTemplate>
                <DataTemplate>
                    <Button Content="{Binding DisplayText}">
                        <i:Interaction.Triggers>
                            <i:EventTrigger EventName="Click">
                                <cal:ActionMessage MethodName="DoAction"/>
                            </i:EventTrigger>
                        </i:Interaction.Triggers>
                    </Button>
                </DataTemplate>
            </ToolBar.ItemTemplate>
        </ToolBar>
    </ToolBarTray>
</UserControl>

class ToolBarItem
    {
        public ToolBarItem(string displayText, Action<object> execute)
            : this(displayText, execute, null)
        {
        }

        public ToolBarItem(string displayText, Action<object> execute, Predicate<object> canExecute)
        {
            this.DisplayText = displayText;
            doAction = execute;
            canDoAction = canExecute;
        }

        public string DisplayText { get; set; }

        private Action<object> doAction;
        public void DoAction()
        {
            doAction(null);
        }

        private Predicate<object> canDoAction;
        public bool CanDoAction()
        {
            if (canDoAction != null)
                return canDoAction(null);
            else
                return true;
        }
There is still only the button supported (see the post of BladeWise), but first things first :)
Do you see a way to simplify this or is there a more accurate way?
Mar 29, 2011 at 12:32 PM
Edited Mar 30, 2011 at 9:21 AM

I would avoid the CanDoAction predicate and stick with a property. Otherwise you need to put some logic to re-evaluate the action availability.

The menu item implementation I am going to use in my project refactor is similar to yours. I use quite a number of interfaces to address extensibility, but the base class is more or less like yours

namespace PROJECT.UI.ViewModels.Menu
{
    #region Namespaces
    using System;
    using Armonia.UI.Menu;

    #endregion

    /// <summary>
    ///   Class used to define a menu item that can execute an action.
    /// </summary>
    public class ActionMenuItem : MenuItem, IActionMenuItem
    {
        #region Fields
        /// <summary>
        ///   The function used to evaluate if the action can be executed.
        /// </summary>
        private readonly Func<bool> m_CanExecute;

        /// <summary>
        ///   The action associated to the menu item.
        /// </summary>
        private readonly Action m_Execute;
        #endregion

        #region Properties
        /// <summary>
        ///   Gets a value indicating whether the action associated to the menu item can be executed.
        /// </summary>
        /// <value>
        ///   <c>True</c> if the action associated to the menu item can be executed; otherwise, <c>false</c>.
        /// </value>
        public bool CanExecute { get; private set; }
        #endregion

        /// <summary>
        ///   Initializes a new instance of the <see cref = "ActionMenuItem" /> class.
        /// </summary>
        /// <param name = "execute">The action associated to the menu item.</param>
        /// <param name = "canExecute">The function used to evaluate if the action can be executed.</param>
        /// <param name = "guid">The menu GUID.</param>
        public ActionMenuItem(Action execute, Func<bool> canExecute = null, Guid? guid = null)
                : base(guid)
        {
            if (execute == null)
                throw new ArgumentNullException("execute");

            m_Execute = execute;
            m_CanExecute = canExecute ?? (() => true);

            ReEvaluateCanExecute();
        }

        #region IActionMenuItem Members
        /// <summary>
        ///   Requests an evaluation of the can execute variable.
        /// </summary>
        public void ReEvaluateCanExecute()
        {
            CanExecute = m_CanExecute();
            NotifyOfPropertyChange(() => CanExecute);
        }

        /// <summary>
        ///   Executes the action associated to the menu item.
        /// </summary>
        public void Execute()
        {
            m_Execute();
        }
        #endregion
    }
}

 

Note the ReEvaluateCanExecute function, use to update the guard state externally.

Coordinator
Mar 29, 2011 at 12:43 PM

Here's some thoughts on how you might improve the API, if you are going to be doing this a lot.

    class Module1ViewModel : Screen, IWorkspace
    {
        [ImportingConstructor]
        public Module1ViewModel(ToolBarViewModel toolBar)
        {
            toolBar.WithScopeOf(this)
                .HasButton("Module1",
                    x => this.Module1ButtonExecute(),
                    x => this.Module1ButtonCanExecute())
                .HasComboBox(...combo details here...)
                .HasItem(...details here...);
        }

        private void Module1ButtonExecute()
        {

        }

        private bool Module1ButtonCanExecute()
        {
            return true;
        }
    }

Here's the general idea. The WithScopeOfMethod creates a toolbar scope which hooks the activated and deactivated events of the screen. This allows the scope to automatically add and remove the items registered with it according to those events. Now you don't have to write that in your VM. The scope could have one method on it called HasItem which takes any object and registers it with the toolbar. The view's data templates or CM's view location can take care of how to render the toolbar item. Finally, a series of extension methods can be built which make creating toolbar items of specific types easier, such as HasButton. Under the covers it would really just call HasItem, creating and passing a button model, then returning the original scope so that you can chain the commands.

Mar 30, 2011 at 8:55 AM
Edited Mar 30, 2011 at 8:56 AM

Thanks for the advise, BladeWise. I  tripped over this problem also and adapted the class accordingly to your model. Works fine now.

Thanks for your good idea, Mr. Eisenberg. I understand what you're describing above, but I don't know, if I could manage to realize this. I'am only an amateur programmer and an autodidact, you know. It seems that I have to learn a lot more, but I will try hard...

Mar 31, 2011 at 8:29 AM

Ok I did the trick (partly):

I had to spend a method "EvaluateCanExecute" for the Action Base, to update the CanExecute Property from ViewModel. The ToolBarViewModel is inheriting from ActionBase and nothing more in it.

Thanks again for the good advices!

 

    public class ActionItem : PropertyChangedBase
    {
        /// <summary>
        /// Initializes a new instance of ActionItem class.
        /// </summary>
        /// <param name="execute"></param>
        /// <param name="canExecute"></param>
        public ActionItem(string content, Action execute, Func<bool> canExecute = null)
        {
            if (execute == null)
                throw new ArgumentNullException("execute");

            this.Content = content;
            this.execute = execute;
            this.canExecute = canExecute ?? (() => true);
            EvaluateCanExecute();
        }

        public string Content { get; private set; }

        private readonly Action execute;
        /// <summary>
        /// The action associated to the ActionItem
        /// </summary>
        public void Execute()
        {
            this.execute();
        }

        private readonly Func<bool> canExecute;
        /// <summary>
        /// Gets a value indication whether the action assiocated to the ActionItem could be executed or not
        /// </summary>
        public bool CanExecute { get; private set; }

        /// <summary>
        /// Updates the value of CanExecute
        /// </summary>
        public void EvaluateCanExecute()
        {
            CanExecute = this.canExecute();
            NotifyOfPropertyChange(() => CanExecute);
        }        
    }

    public class ActionBase
    {
        public ActionBase()
        {
            Items = new BindableCollection<ActionItem>();
            actionScopes = new List<ActionScope>();
        }
        
        private List<ActionScope> actionScopes;

        public BindableCollection<ActionItem> Items { get; private set; }

        public ActionScope WithScopeOf(Screen screen)
        {
            ActionScope scope = new ActionScope(this.Items);
            screen.Activated += scope.Activated;
            screen.Deactivated += scope.Deactivated;
            actionScopes.Add(scope);
            
            return scope;
        }

        /// <summary>
        /// Updates the value of CanExecute of all active ActionItems
        /// </summary>
        public void EvaluateCanExecute()
        {
            foreach (ActionItem item in Items)
            {
                item.EvaluateCanExecute();
            }
        }
    }

    public class ActionScope
    {
        public ActionScope(BindableCollection<ActionItem> items)
        {
            this.actionBaseItems = items;
            this.ActionItems = new List<ActionItem>();
        }

        private BindableCollection<ActionItem> actionBaseItems;

        private List<ActionItem> ActionItems;

        public ActionScope HasItem(ActionItem item)
        {
            ActionItems.Add(item);
            return this;
        }

        private bool hasActiveItems = false;
        public void Activated(object source, ActivationEventArgs e)
        {
            actionBaseItems.AddRange(ActionItems);
            hasActiveItems = true;
        }

        public void Deactivated(object source, DeactivationEventArgs e)
        {
            if (hasActiveItems)
                actionBaseItems.RemoveRange(ActionItems);
            hasActiveItems = false;
        }
    }


//Part of the ShellViewModel follows here

            ToolBar.WithScopeOf(this)
                   .HasItem(new ActionItem("OpenModule1", openModule1, canOpenModule1))
                   .HasItem(new ActionItem("OpenModule2", openModule2));