C# Lifecycle User Control Promote Demote

C# Lifecycle User Control Promote Demote

Yesterday I created a User Control for a simple Life Cycle graph (here), I though it would be fun to add some user input to allow Promotion and Demotion, to do this I’m going to use the Messaging Class I talked about early this week (here).

Status Model

Since the Context Menu that will provide a Promote and a Demote option interacts directly with the Status Model, we must add our command methods here. Additionally in the constructor I’m passing in the Messenger object, so I can use the Send method.

using System.Windows;
using System.Windows.Input;
using User_Controls.Commands.Base;
using User_Controls.Messages;

namespace User_Controls.Models
{
    public class StatusModel
    {
        public bool StatusState { get; set; } = false;
        public string StatusTitle { get; set; } = "Frozen";      
        public Visibility StatusArrowVisibility { get; set; } = Visibility.Visible;

        private readonly IMessenger? _messenger = null;

        public StatusModel(IMessenger messenger)
        {
            _messenger = messenger;
        }

        public ICommand StatusPromoteCommand => new CommandsBase(StatusPromoteCommandExecute);

        private void StatusPromoteCommandExecute(object? obj)
        {
            _messenger?.Send(new PromoteMessage(this));
        }

        public ICommand StatusDemoteCommand => new CommandsBase(StatusDemoteCommandExecute);

        private void StatusDemoteCommandExecute(object? obj)
        {
            _messenger?.Send(new DemoteMessage(this));
        }
    }
}

Commands Base

I use the following Commands Base for all of my Commands.

using System.Windows.Input;

namespace User_Controls.Commands.Base
{
    public class CommandsBase : ICommand
    {
        private readonly Action<object?> _executeAction;
        private readonly Predicate<object?>? _canExecuteAction;

        public CommandsBase(Action<object?> executeAction)
        {
            _executeAction = executeAction;
            _canExecuteAction = null;
        }

        public CommandsBase(Action<object?> executeAction, Predicate<object?> canExecute)
        {
            _executeAction = executeAction;
            _canExecuteAction = canExecute;
        }

        public event EventHandler? CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public bool CanExecute(object? parameter)
        {
            return _canExecuteAction == null ? true : _canExecuteAction(parameter);
        }
        public void Execute(object? parameter)
        {
            if (_executeAction != null)
            {
                _executeAction(parameter);
            }
        }
    }
}

Messages

To send a Promote or Demote message we will need to create two new record objects for the Promote and Demote Messages, and in both cases well pass back the current selected Status Model, so we know which status model must become active and inactive

using User_Controls.Models;

namespace User_Controls.Messages
{
    public record PromoteMessage(StatusModel currentStatusModel);
    public record DemoteMessage(StatusModel currentStatusModel);
}

View Model

In the View Model we will subscribe to the Two Messages and create two methods to handle the Promotion and Demotion.

public MainViewModel(
                        ILogger logger,
                        IMessenger messenger,
                        MainWindow mainWindow
                        )
{
    logger.Information("Application Started.");

    _logger = logger;
    _messenger = messenger;
    _messenger.Subscribe<PromoteMessage>(this, OnPromotionChanged);
    _messenger.Subscribe<DemoteMessage>(this, OnDemotionChanged);
    MainWindow = mainWindow;


    StatusModels = new ObservableCollection<StatusModel>()
    {
        new StatusModel(_messenger){StatusTitle = "Frozen"  , StatusState = false },
        new StatusModel(_messenger){StatusTitle = "Online"  , StatusState = false },
        new StatusModel(_messenger){StatusTitle = "Offline" , StatusState = true  },
        new StatusModel(_messenger){StatusTitle = "Busy"    , StatusState = false , StatusArrowVisibility = Visibility.Hidden}
    };

}

private void OnPromotionChanged(object obj)
{
    if (obj is PromoteMessage message && StatusModels != null)
    {
        ObservableCollection<StatusModel> statusModels = StatusModels;
        StatusModel statusModel = message.currentStatusModel;
        int currentPosition = statusModels.IndexOf(statusModel);

        if (currentPosition < statusModels.Count-1)
        {
            statusModels[currentPosition].StatusState = false;
            statusModels[currentPosition + 1].StatusState = true;
            StatusModels = new ObservableCollection<StatusModel>(statusModels);
        }
    }
}

private void OnDemotionChanged(object obj)
{
    if (obj is DemoteMessage message && StatusModels!= null)
    {
        ObservableCollection<StatusModel> statusModels = StatusModels;
        StatusModel statusModel = message.currentStatusModel;
        int currentPosition = statusModels.IndexOf(statusModel);
        if (currentPosition > 0)
        {
            statusModels[currentPosition].StatusState = false;
            statusModels[currentPosition - 1].StatusState = true;
            StatusModels = new ObservableCollection<StatusModel>(statusModels);
        }
    }
}

StateUC.XAML

in the first user Control that defines the TextBlock we will add a ContextMenu to the Border tag, for Promote and Demote and Bind them to the two Commands in the Status Model.

    <Border.ContextMenu>
        <ContextMenu>
            <MenuItem 
                Header="Promote" 
                Command="{Binding StatusPromoteCommand}"/>
            <MenuItem 
                Header="Demote" 
                Command="{Binding StatusDemoteCommand}"/>
        </ContextMenu>
    </Border.ContextMenu>
</Border>

Final Result

Now when we Right Mouse Click on each of the Nodes we can Promote and Demote the Status of the Life Cycle.

Promote Demote

Below we can see that the lifecycle has been promoted to Busy.

Busy Status

Controlling the Visibility of the Context Menu

We only really need to display the context menu on the Active Status Node. We can do this by creating a Context Menu Style and Data Trigger, that is bound to the Status State in the Status Model.

<Border.ContextMenu>
    <ContextMenu>
        <MenuItem 
            Header="Promote" 
            Command="{Binding StatusPromoteCommand}"/>
        <MenuItem 
            Header="Demote" 
            Command="{Binding StatusDemoteCommand}"/>
        <ContextMenu.Style>
            <Style TargetType="ContextMenu">
                <Style.Triggers>
                    <DataTrigger Binding="{Binding StatusState, UpdateSourceTrigger=PropertyChanged,Mode=TwoWay}" Value="True">
                        <Setter Property="Visibility" Value="Visible"/>
                    </DataTrigger>
                    <DataTrigger Binding="{Binding StatusState, UpdateSourceTrigger=PropertyChanged,Mode=TwoWay}" Value="False">
                        <Setter Property="Visibility" Value="Hidden"/>
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </ContextMenu.Style>
    </ContextMenu>
</Border.ContextMenu>

We can take this one step further by adding an extra int property into the Status Model. If the Value is -1 its the first node, if the value is 1 its the last node and if the value is 0 its a node in-between. I’m sure there is a better way to do this but its late.


We can then add the additional Data Triggers.

<Border.ContextMenu>
    <ContextMenu>
        <MenuItem 
            Header="Promote" 
            Command="{Binding StatusPromoteCommand}">
            <MenuItem.Style>
                <Style TargetType="MenuItem">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding FirstLast, UpdateSourceTrigger=PropertyChanged,Mode=TwoWay}" Value="1">
                            <Setter Property="Visibility" Value="Collapsed"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </MenuItem.Style>
        </MenuItem>
        <MenuItem 
            Header="Demote" 
            Command="{Binding StatusDemoteCommand}">
            <MenuItem.Style>
                <Style TargetType="MenuItem">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding FirstLast, UpdateSourceTrigger=PropertyChanged,Mode=TwoWay}" Value="-1">
                            <Setter Property="Visibility" Value="Collapsed"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </MenuItem.Style>
        </MenuItem>
        <ContextMenu.Style>
            <Style TargetType="ContextMenu">
                <Style.Triggers>
                    <DataTrigger Binding="{Binding StatusState, UpdateSourceTrigger=PropertyChanged,Mode=TwoWay}" Value="True">
                        <Setter Property="Visibility" Value="Visible"/>
                    </DataTrigger>
                    <DataTrigger Binding="{Binding StatusState, UpdateSourceTrigger=PropertyChanged,Mode=TwoWay}" Value="False">
                        <Setter Property="Visibility" Value="Hidden"/>
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </ContextMenu.Style>
    </ContextMenu>
</Border.ContextMenu>

The result we get is.