Zoeken

Plugins tutorial 3: TLCGen events

Deze tutorial gaat verder waar de tutorial rond data opslag gebleven was. Volg deze dus eerst, of download de code om direct te beginnen met deze tutorial, op basis van de code uit de vorige tutorial.

De resulterende code van deze tutorial is hier te downloaden.

Inhoud:

Stap 1: uitbreiden GUI

In de huidige opzet is de GUI van de plugin slechts een leeg tabblad, zonder verdere functie. Nu de plugin de mogelijkheid heeft data op te slaan, moet de gebruiker instaat worden gesteld de data te bewerken. We zullen hiertoe werken met WPF, en gebruik maken van het zogenaamde MVVM (Model-View-ViewModel) ontwerp patroon. Dit valt grotendeels buiten de scope van deze tutorial, en zal dus niet met veel details worden beschreven. Onderstaand is dus ter informatie, om een beeld te krijgen van hoe een dergelijke opbouw eruit kan zien. Merk op dat er veel verschillende interpretaties van MVVM mogelijk zijn, waarvan dit er één is.

Stap 1a: aanpassen ontwerp GUI

We passen eerste het ontwerp van de GUI zelf aan, door het berwerken van de XAML code, in bestand GroenBegrenzerTabView.xaml. Als volgt:

<UserControl x:Class="TLCGen.Plugins.GroenBegrenzer.GroenBegrenzerTabView"
             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:local="clr-namespace:TLCGen.Plugins.GroenBegrenzer"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Label Content="Signaalgroepen met groenbegrenzing" />
        <DataGrid ItemsSource="{Binding Path=SignaalGroepen}" AutoGenerateColumns="False" CanUserAddRows="False" CanUserDeleteRows="False"
                  Grid.Row="1">
            <DataGrid.RowHeaderTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor, 
                                      AncestorType={x:Type DataGridRow}}, 
                                      Path=Item.SignalGroupName}"/>
                </DataTemplate>
            </DataGrid.RowHeaderTemplate>
            <DataGrid.Columns>
                <DataGridCheckBoxColumn Binding="{Binding Path=BegrensGroen,UpdateSourceTrigger=PropertyChanged}" Header="Begrens" />
                <DataGridTextColumn Binding="{Binding Path=MaximaalGroen,UpdateSourceTrigger=PropertyChanged}" Header="Maximaal groen">
                    <DataGridTextColumn.CellStyle>
                        <Style TargetType="DataGridCell">
                            <Setter Property="IsEnabled" Value="{Binding Path=BegrensGroen}" />
                        </Style>
                    </DataGridTextColumn.CellStyle>
                </DataGridTextColumn>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</UserControl>

Deze XAML code zorgt ervoor dat er een datagrid (tabel met data) verschijnt op het tabblad, met twee kolommen, ‘Begrens’ en ‘Maximaal groen’. De data voor de weergave is afkomstig uit de lijst ‘SignaalGroepen’, die we verderop middels data binding aan de GUI zullen verbinden. De code zorgt er verder voor dat elke regel in de tabel een header krijgt met de naam van de signaalgroep, en dat het veld ‘Maximaal groen’ alleen beschikbaar komt wanneer ‘Begrens’ is aangevinkt.

Stap 1b: verbinden data met de GUI (data binding)

We maken nu een nieuwe class aan, bv. met de naam ‘GroenBegrenzerTabViewModel’. Voor het overzicht kan het handig zijn de ViewModels in een aparte map in het project te plaatsen.

Het ViewModel is ervoor verantwoordelijk te reageren op acties van de gebruiker in de GUI, en wijzigingen te verwerken. Ook zorgt het ervoor dat wijzingen van elders (bv. toevoegen van fasen), in de GUI terug te zien zijn. Het ViewModel moet daarp, toegang hebben tot het data model van de plugin, en het is tevens handig een referentie op te slaan naar de plugin class zelf. Bijvoorbeeld als volgt:

using GalaSoft.MvvmLight;
using TLCGen.Helpers;
using TLCGen.Plugins.GroenBegrenzer.Models;

namespace TLCGen.Plugins.GroenBegrenzer.ViewModels
{
    public class GroenBegrenzerTabViewModel : ViewModelBase
    {
        #region Fields
        private GroenBegrenzerPlugin _plugin;
        private GroenBegrenzerDataModel _data;
        #endregion // Fields

        #region Properties
        public GroenBegrenzerDataModel Data
        {
            get => _data;
            set
            {
                _data = value;
                if (_data != null)
                {
                    SignaalGroepen = new ObservableCollectionAroundList<GroenBegrenzerSignaalGroepDataViewModel, GroenBegrenzerSignaalGroepDataModel>(_data.SignaalGroepen);
                }
                else
                {
                    SignaalGroepen = null;
                }
                RaisePropertyChanged(nameof(SignaalGroepen));
            }
        }
        public ObservableCollectionAroundList<GroenBegrenzerSignaalGroepDataViewModel, GroenBegrenzerSignaalGroepDataModel> SignaalGroepen { get; private set; }
        #endregion // Properties

        #region Constructor
        public GroenBegrenzerTabViewModel(GroenBegrenzerPlugin plugin)
        {
            _plugin = plugin;
        }
        #endregion // Constructor
    }
}

We zien in deze class de property “SignaalGroepen”, waar we eerder in de XAML code aan hebben gerefereerd.

De class wordt afgeleid van ‘ViewModelBase’, een class uit de library “MvvmLightLibs”. Voeg deze toe aan het project middels Nuget (zie bv hier). Zoek daartoe naar ‘mvvmlightlibs’, waarbij het libs gedeelte niet moet ontbreken.

Er wordt gebruik gemaakt van de class “ObservableCollectionAroundList”. Dit is een helper class uit TLCGen, die het vergemakkelijkt een List uit het model weer te geven in de GUI middels een ObservableCollection: wijzigingen in die laatste worden vervolgens automatisch doorgevoerd in de achterliggende List. Om dit mogelijk te maken moet de class GroenBegrenzerSignaalGroepDataViewModel, die we nog moeten maken, het interface IViewModelWithItem implementeren. De class ziet er bijv. als volgt uit:

using GalaSoft.MvvmLight;
using System;
using TLCGen.Helpers;
using TLCGen.Plugins.GroenBegrenzer.Models;

namespace TLCGen.Plugins.GroenBegrenzer.ViewModels
{
    public class GroenBegrenzerSignaalGroepDataViewModel : ViewModelBase, IViewModelWithItem, IComparable
    {
        public GroenBegrenzerSignaalGroepDataModel GroenBegrenzerSignaalGroep { get; }

        public string SignalGroupName
        {
            get => GroenBegrenzerSignaalGroep.SignaalGroepNaam;
            set
            {
                GroenBegrenzerSignaalGroep.SignaalGroepNaam = value;
                RaisePropertyChanged<object>(broadcast: true);
            }
        }

        public bool BegrensGroen
        {
            get => GroenBegrenzerSignaalGroep.BegrensGroen;
            set
            {
                GroenBegrenzerSignaalGroep.BegrensGroen = value;
                RaisePropertyChanged<object>(broadcast: true);
            }
        }

        public int MaximaalGroen
        {
            get => GroenBegrenzerSignaalGroep.MaximaalGroen;
            set
            {
                GroenBegrenzerSignaalGroep.MaximaalGroen = value;
                RaisePropertyChanged<object>(broadcast: true);
            }
        }

        public GroenBegrenzerSignaalGroepDataViewModel(GroenBegrenzerSignaalGroepDataModel groenBegrenzerSignaalGroep)
        {
            GroenBegrenzerSignaalGroep = groenBegrenzerSignaalGroep;
        }

        public object GetItem()
        {
            return GroenBegrenzerSignaalGroep;
        }

        public int CompareTo(object obj)
        {
            return SignalGroupName.CompareTo(((GroenBegrenzerSignaalGroepDataViewModel)obj).SignalGroupName);
        }
    }
}

Deze class draagt er zorg voor, dat wijzigingen in de GUI ook terug komen in het data model. De class implementeerd ook IComparable, zodat we de lijst kunnen sorteren op basis van de namen van signaalgroepen.

We moeten nu nog zorgen het viewmodel wordt opgebouwd, de beschikking krijgt over het data model, en als DataContext wordt ingesteld voor de View (de GUI zelf), ofwel we moeten de data binding organiseren. Als eerste voegen we field toe boven aan de plugin class GroenBegrenzerPlugin:

private GroenBegrenzerTabViewModel _dataViewModel;

Vervolgens passen we de code van het DataTemplate aan:

DataTemplate _ContentDataTemplate;
public DataTemplate ContentDataTemplate
{
    get
    {
        if (_ContentDataTemplate == null)
        {
            _ContentDataTemplate = new DataTemplate();
            var tab = new FrameworkElementFactory(typeof(GroenBegrenzerTabView));
            tab.SetValue(FrameworkElement.DataContextProperty, _dataViewModel);
            _ContentDataTemplate.VisualTree = tab;        
        }
        return _ContentDataTemplate;
    }
}

Hier wordt het ViewModel ingesteld als DataContext voor de View, zodat data binding kan functioneren.

Ten slotte passen we de functie UpdateModel aan, zodat het viewmodel toegang krijgt tot het model:

private void UpdateModel()
{
    // controller with plugin data model
    if (_controller != null && _dataModel != null)
    {
        foreach(var nfc in _controller.Fasen.Where(x => _dataModel.SignaalGroepen.All(x2 => x2.SignaalGroepNaam != x.Naam)))
        {
            _dataModel.SignaalGroepen.Add(new GroenBegrenzerSignaalGroepDataModel { SignaalGroepNaam = nfc.Naam });
        }
        var oldFcs = _dataModel.SignaalGroepen.Where(x => _controller.Fasen.All(x2 => x2.Naam != x.SignaalGroepNaam)).ToList();
        foreach (var ofc in oldFcs)
        {
            _dataModel.SignaalGroepen.Remove(ofc);
        }
        _dataViewModel.Data = _dataModel;
    }
    // controller without plugin data model
    else if(_controller != null)
    {
        _dataModel = new GroenBegrenzerDataModel();
        _dataViewModel.Data = _dataModel;
    }
}

De GUI en de interactie tussen de GUI (View) en de data (Model) zijn geregeld (middels het ViewModel). Wat echter nog ontbreekt is het reageren op acties binnen TLCGen, bijvoorbeeld wanneer een signaalgroep wordt toegevoegd, of de naam van een signaalgroep wordt gewijzigd.

Stap 2: reageren op events uit TLCGen

TLCGen stuurt berichten uit wanneer bepaalde dingen gebeuren. Voorbeelden hiervan die relevant zijn voor de huidige plugin zijn: fasen worden toegevoegd of verwijderd, er veranderd een naam, of fasen worden gesorteerd. TLCGen maakt hiertoe gebruik van de Messenger class die met de MvvmLight toolkit wordt meegeleverd. Om een idee te krijgen van het type berichten dat beschikbaar is: kijk naar de classes in de map ‘Messaging’ in het TLCGen.Dependencies project binnen de source code van TLCGen. Zo zijn er berichten omtrent wijzigingen van detectoren, groentijden, ingangen, intersignaalgroep data, ov ingrepen, perioden, etc. Het is ook mogelijk zelf berichten te sturen, bv. het ControllerDataChangedMessage bericht om aan te geven dat er een wijziging is geweest die kan worden opgeslagen (dit zorgt er ook voor dat TLCGen vraagt om opslaan bij sluiten van de regeling).

Om de plugin in staat te stellen hierop te reageren moeten we ons inschrijven op deze events. We breiden daartoe de plugin class uit met een extra interface, en voegen ook een vlag toe aan het TLCGenPlugin attribuut:

[TLCGenPlugin(TLCGenPluginElems.TabControl | TLCGenPluginElems.XMLNodeWriter | TLCGenPluginElems.PlugMessaging)]
[TLCGenTabItem(-1, TabItemTypeEnum.MainWindow)]
public class GroenBegrenzerPlugin : ITLCGenTabItem, ITLCGenXMLNodeWriter, ITLCGenPlugMessaging

De interface ITLCGenPlugMessaging heeft slechts één methode. Om het overzcihtelijk te houden zullen we het reageren op events oppakken in het ViewModel.

public void UpdateTLCGenMessaging()
{
    _dataViewModel.UpdateTLCGenMessaging();
}

In het ViewModel voegen we de betreffende functie toe en registeren ons voor de relevante events. Hieronder wordt direct de code weergegeven voor het reageren.

#region Public methods
public void UpdateTLCGenMessaging()
{
    MessengerInstance.Register(this, new Action<FasenChangedMessage>(OnFasenChanged));
    MessengerInstance.Register(this, new Action<NameChangedMessage>(OnNameChanged));
    MessengerInstance.Register(this, new Action<FasenSortedMessage>(OnFasenSorted));
}
#endregion // Public methods

#region Private methods
private void OnFasenSorted(FasenSortedMessage obj)
{
    if (SignaalGroepen == null) return;
    SignaalGroepen.BubbleSort();
}

private void OnNameChanged(NameChangedMessage obj)
{
    if (SignaalGroepen == null) return;
    if (obj.ObjectType == TLCGen.Models.Enumerations.TLCGenObjectTypeEnum.Fase)
    {
        var of = SignaalGroepen.FirstOrDefault(x => x.SignalGroupName == obj.OldName);
        if (of != null) of.SignalGroupName = obj.NewName;
    }
}

private void OnFasenChanged(FasenChangedMessage obj)
{
    if (SignaalGroepen == null) return;
    if (obj.RemovedFasen != null && obj.RemovedFasen.Any())
    {
        foreach (var fc in obj.RemovedFasen)
        {
            var rfc = SignaalGroepen.FirstOrDefault(x => x.SignalGroupName == fc.Naam);
            if (rfc != null) SignaalGroepen.Remove(rfc);
        }
    }
    if (obj.AddedFasen != null && obj.AddedFasen.Any())
    {
        foreach (var fc in obj.AddedFasen)
        {
            SignaalGroepen.Add(new GroenBegrenzerSignaalGroepDataViewModel(new GroenBegrenzerSignaalGroepDataModel { SignaalGroepNaam = fc.Naam }));
        }
    }
}
#endregion // Private methods

Wanneer wordt gesorteerd, sorteert de plugin ook de fasen. BubbleSort is een help functie (extensie) uit TLCGen. Bij het wijzigen van een naam wordt gekeken of het een signaalgroep betreft, en zo ja, wordt de naam ook in ons model doorgevoerd. Bij toevoegen of verwijderen van fasen wordt de wijziging ook in het data model doorgevoerd. Door dit te regelen in het ViewModel wordt de weergave richting de gebruiker ook direct aangepast.

We zijn nu klaar om de wering te testen: maak een regeling aan, en voeg een fase toe; deze zal ook in de lijst van de plugin tevoorschijn komen. Hetzelfde geldt voor wijzigingen in de naamgeving, sorteren en verwijderen van fasen.

De plugin kan inmiddels data opslaan, weergeven richting de gebruiker, staat bewerken toe, en reageert adequaat op wijzigingen in het bijbehorende controller model. Het enige wat in de plugin nu nog ontbreekt, is het genereren van code. Dit volgt in de volgende tutorial.

Inhoudsopgave