浏览代码

Made trigger grid

Kenric Nugteren 8 月之前
父节点
当前提交
fd50ab8242

+ 26 - 7
prs.shared/Events/Event.cs

@@ -28,7 +28,7 @@ public class EventData<T, TDataModel> : IEventData
     /// <summary>
     /// A list of triggers for this event. If any of the triggers match, the event runs.
     /// </summary>
-    public List<ITrigger<T, TDataModel>> Triggers { get; set; }
+    public List<IEventTrigger<T, TDataModel>> Triggers { get; set; }
 
     public List<IEventAction<T>> Actions { get; set; }
 
@@ -37,7 +37,7 @@ public class EventData<T, TDataModel> : IEventData
     public EventData(T eventData)
     {
         Event = eventData;
-        Triggers = new List<ITrigger<T, TDataModel>>();
+        Triggers = new List<IEventTrigger<T, TDataModel>>();
         Actions = new List<IEventAction<T>>();
     }
 
@@ -64,7 +64,7 @@ public class EventData<T, TDataModel> : IEventData
         var nTriggers = reader.ReadInt32();
         for(int i = 0; i < nTriggers; ++i)
         {
-            var trigger = EventUtils.DeserializeObject<ITrigger<T, TDataModel>>(EventUtils.GetTriggerType, reader);
+            var trigger = EventUtils.DeserializeObject<IEventTrigger<T, TDataModel>>(EventUtils.GetTriggerType, reader);
             Triggers.Add(trigger);
         }
         var nActions = reader.ReadInt32();
@@ -159,10 +159,12 @@ public static class EventUtils
     private static Dictionary<string, Type>? _triggerTypes;
     private static Dictionary<string, Type>? _actionTypes;
 
-    [MemberNotNullWhen(true, nameof(_eventTypes), nameof(_triggerTypes), nameof(_actionTypes))]
+    private static Dictionary<Type, List<Type>>? _eventTriggerTypes;
+
+    [MemberNotNullWhen(true, nameof(_eventTypes), nameof(_triggerTypes), nameof(_actionTypes), nameof(_eventTriggerTypes))]
     private static bool _loadedTypes { get; set; }
 
-    [MemberNotNull(nameof(_eventTypes), nameof(_triggerTypes), nameof(_actionTypes))]
+    [MemberNotNull(nameof(_eventTypes), nameof(_triggerTypes), nameof(_actionTypes), nameof(_eventTriggerTypes))]
     private static void LoadTypes()
     {
         if (_loadedTypes) return;
@@ -171,6 +173,7 @@ public static class EventUtils
         _eventTypes = new();
         _triggerTypes = new();
         _actionTypes = new();
+        _eventTriggerTypes = new();
         foreach(var type in CoreUtils.TypeList(x => true))
         {
             if (type.HasInterface(typeof(IEvent<>)))
@@ -180,11 +183,15 @@ public static class EventUtils
                     _eventTypes[type.Name] = type;
                 }
             }
-            else if (type.HasInterface(typeof(ITrigger<,>)))
+            else if (type.GetInterfaceDefinition(typeof(IEventTrigger<,>)) is Type eventInterface)
             {
                 if (type.GetConstructor([]) is not null)
                 {
                     _triggerTypes[type.Name] = type;
+
+                    var eventType = eventInterface.GenericTypeArguments[0];
+                    eventType = eventType.IsGenericType ? eventType.GetGenericTypeDefinition() : eventType;
+                    _eventTriggerTypes.GetValueOrAdd(eventType).Add(type);
                 }
             }
             else if (type.HasInterface(typeof(IEventAction<>)))
@@ -197,6 +204,13 @@ public static class EventUtils
         }
     }
 
+    public static IEnumerable<Type> GetEventTriggerTypes(Type eventType)
+    {
+        LoadTypes();
+        eventType = eventType.IsGenericType ? eventType.GetGenericTypeDefinition() : eventType;
+        return _eventTriggerTypes.GetValueOrDefault(eventType) ?? Enumerable.Empty<Type>();
+    }
+
     public static Type GetEventType(string type)
     {
         LoadTypes();
@@ -448,7 +462,12 @@ public interface IEvent<TDataModel> : IEvent
     Notification GenerateNotification(TDataModel model);
 }
 
-public interface ITrigger<TEvent, TDataModel> : ISerializeBinary
+public interface IEventTrigger
+{
+    string GetDescription();
+}
+
+public interface IEventTrigger<TEvent, TDataModel> : ISerializeBinary, IEventTrigger
     where TEvent : IEvent<TDataModel>
     where TDataModel : IEventDataModel
 {

+ 38 - 3
prs.shared/Events/SaveEvent.cs

@@ -104,7 +104,8 @@ public class SaveEventDataModel<T>(T entity) : IEventDataModel, ITypedEventDataM
 
 #region Triggers
 
-public class CreatedSaveEventTrigger<T> : ITrigger<SaveEvent<T>, SaveEventDataModel<T>>
+[Caption("New Record")]
+public class CreatedSaveEventTrigger<T> : IEventTrigger<SaveEvent<T>, SaveEventDataModel<T>>
     where T : Entity
 {
     public bool Check(SaveEventDataModel<T> dataModel)
@@ -118,9 +119,12 @@ public class CreatedSaveEventTrigger<T> : ITrigger<SaveEvent<T>, SaveEventDataMo
     public void DeserializeBinary(CoreBinaryReader reader)
     {
     }
+
+    public string GetDescription() => "New Record";
 }
 
-public class PropertyChangedSaveEventTrigger<T> : ITrigger<SaveEvent<T>, SaveEventDataModel<T>>
+[Caption("Property Changed")]
+public class PropertyChangedSaveEventTrigger<T> : IEventTrigger<SaveEvent<T>, SaveEventDataModel<T>>
     where T : Entity
 {
     public IProperty? TriggerProperty { get; set; }
@@ -173,9 +177,34 @@ public class PropertyChangedSaveEventTrigger<T> : ITrigger<SaveEvent<T>, SaveEve
             TriggerProperty = null;
         }
     }
+
+    public string GetDescription()
+    {
+        if(TriggerProperty is null)
+        {
+            return $"{typeof(T).GetCaption()} changed";
+        }
+        else if(OldValue is null && NewValue is null)
+        {
+            return $"{typeof(T).GetCaption()}.{TriggerProperty.Name} changed";
+        }
+        else if (OldValue is null)
+        {
+            return $"{typeof(T).GetCaption()}.{TriggerProperty.Name} changed to {NewValue}";
+        }
+        else if (NewValue is null)
+        {
+            return $"{typeof(T).GetCaption()}.{TriggerProperty.Name} changed from {OldValue}";
+        }
+        else
+        {
+            return $"{typeof(T).GetCaption()}.{TriggerProperty.Name} changed from {OldValue} to {NewValue}";
+        }
+    }
 }
 
-public class ScriptSaveEventTrigger<T> : ITrigger<SaveEvent<T>, SaveEventDataModel<T>>
+[Caption("Custom Script")]
+public class ScriptSaveEventTrigger<T> : IEventTrigger<SaveEvent<T>, SaveEventDataModel<T>>
     where T : Entity
 {
     private ScriptDocument? _scriptDocument;
@@ -225,6 +254,7 @@ public class Module
     {
         writer.Write(Script ?? "");
     }
+
     public void DeserializeBinary(CoreBinaryReader reader)
     {
         var script = reader.ReadString();
@@ -237,12 +267,15 @@ public class Module
             Script = script;
         }
     }
+
+    public string GetDescription() => "Custom Script";
 }
 
 #endregion
 
 #region Actions
 
+[Caption("Custom Script")]
 public class ScriptSaveEventAction<T> : IEventAction<SaveEvent<T>>
     where T : Entity
 {
@@ -310,6 +343,7 @@ public class Module
     {
         writer.Write(Script ?? "");
     }
+
     public void DeserializeBinary(CoreBinaryReader reader)
     {
         var script = reader.ReadString();
@@ -324,6 +358,7 @@ public class Module
     }
 }
 
+[Caption("Create Entity")]
 public class CreateEntityAction<T> : IEventAction<SaveEvent<T>>
     where T : Entity
 {

+ 66 - 0
prs.shared/Grids/EventEditor/EventEditor.xaml

@@ -0,0 +1,66 @@
+<UserControl x:Class="PRS.Shared.EventEditor"
+        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        xmlns:local="clr-namespace:PRS.Shared"
+        xmlns:WPF="clr-namespace:InABox.WPF;assembly=InABox.Wpf"
+        mc:Ignorable="d" x:Name="Editor">
+    <UserControl.Resources>
+        <WPF:BooleanToVisibilityConverter x:Key="boolToVisibility"/>
+    </UserControl.Resources>
+    <Grid Margin="0" DataContext="{Binding ElementName=Editor}"
+          MinHeight="400">
+        <Grid.RowDefinitions>
+            <RowDefinition Height="Auto"/>
+            <RowDefinition Height="*"/>
+        </Grid.RowDefinitions>
+        <Grid.ColumnDefinitions>
+            <ColumnDefinition Width="*"/>
+        </Grid.ColumnDefinitions>
+        <DockPanel Grid.Row="0" LastChildFill="False" Margin="0,0,0,5">
+            <ComboBox x:Name="EventTypeBox" DockPanel.Dock="Left" Padding="5"
+                      SelectedValue="{Binding EventType}" MinWidth="50"/>
+
+            <Label x:Name="EntityTypeLabel" DockPanel.Dock="Left" Margin="5,0,0,0"
+                   Content="Entity Type:"
+                   Visibility="{Binding HasType,Converter={StaticResource boolToVisibility}}"/>
+            <ComboBox x:Name="EntityTypeBox" DockPanel.Dock="Left" Padding="5" Margin="5,0,0,0"
+                      Visibility="{Binding HasType,Converter={StaticResource boolToVisibility}}"
+                      MinWidth="100"
+                      DisplayMemberPath="Name"
+                      SelectedValue="{Binding EntityType}"/>
+        </DockPanel>
+        <ContentControl x:Name="Content" Grid.Row="1" Visibility="Collapsed"/>
+        <Grid x:Name="Placeholder" Grid.Row="1">
+            <Grid.RowDefinitions>
+                <RowDefinition Height="Auto"/>
+                <RowDefinition Height="*"/>
+            </Grid.RowDefinitions>
+            <Grid.ColumnDefinitions>
+                <ColumnDefinition Width="*"/>
+                <ColumnDefinition Width="Auto"/>
+                <ColumnDefinition Width="*"/>
+            </Grid.ColumnDefinitions>
+            <StackPanel Orientation="Horizontal"
+                        Grid.Row="0" Grid.RowSpan="2" Grid.Column="1">
+                <Separator Style="{StaticResource {x:Static ToolBar.SeparatorStyleKey}}"
+                           Margin="3,0,3,0"/>
+            </StackPanel>
+            <Border BorderBrush="Gray" BorderThickness="0.75" Background="WhiteSmoke"
+                    Grid.Row="0" Grid.Column="0">
+                <Label Content="Triggers" VerticalAlignment="Center" HorizontalAlignment="Center"/>
+            </Border>
+            <Border BorderBrush="Gray" BorderThickness="0.75" Background="WhiteSmoke"
+                    Grid.Row="0" Grid.Column="2">
+                <Label Content="Actions" VerticalAlignment="Center" HorizontalAlignment="Center"/>
+            </Border>
+            <ContentControl x:Name="TriggersControl" Grid.Row="1" Grid.Column="0" Margin="0,5,0,0">
+                <Border BorderBrush="Gray" BorderThickness="0.75" Background="White" />
+            </ContentControl>
+            <ContentControl x:Name="ActionsControl" Grid.Row="1" Grid.Column="2" Margin="0,5,0,0">
+                <Border BorderBrush="Gray" BorderThickness="0.75" Background="White" />
+            </ContentControl>
+        </Grid>
+    </Grid>
+</UserControl>

+ 157 - 0
prs.shared/Grids/EventEditor/EventEditor.xaml.cs

@@ -0,0 +1,157 @@
+using Comal.Classes;
+using InABox.Core;
+using InABox.DynamicGrid;
+using PRS.Shared.Events;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Shapes;
+
+namespace PRS.Shared;
+/// <summary>
+/// Interaction logic for EventEditorWIndow.xaml
+/// </summary>
+public partial class EventEditor : UserControl, INotifyPropertyChanged
+{
+    private EventType? _eventType;
+    public EventType? EventType
+    {
+        get => _eventType;
+        set
+        {
+            _eventType = value;
+            OnPropertyChanged();
+
+            HasType = value.HasValue && EventTypeHasEntityType(value.Value);
+
+            UpdateEventData();
+        }
+    }
+
+    private Type? _entityType;
+    public Type? EntityType
+    {
+        get => _entityType;
+        set
+        {
+            _entityType = value;
+            OnPropertyChanged();
+
+            UpdateEventData();
+        }
+    }
+
+    public bool HasType { get; set; }
+
+    private Type? EventDataType;
+    private Type? DataModelType;
+
+    private IEventData? _data;
+    public IEventData? Data
+    {
+        get => _data;
+        set
+        {
+            _data = value;
+            OnPropertyChanged();
+
+            if(value is not null && EventDataType is not null && DataModelType is not null)
+            {
+                var control = Activator.CreateInstance(typeof(EventEditorControl<,>).MakeGenericType(EventDataType, DataModelType), value);
+                Content.Content = control;
+                Content.Visibility = Visibility.Visible;
+                Placeholder.Visibility = Visibility.Collapsed;
+            }
+            else
+            {
+                Content.Content = null;
+                Content.Visibility = Visibility.Collapsed;
+                Placeholder.Visibility = Visibility.Visible;
+            }
+        }
+    }
+
+    public EventEditor()
+    {
+        InitializeComponent();
+
+        EventTypeBox.ItemsSource = Enum.GetValues<EventType>();
+
+        var entities = CoreUtils.Entities.Where(x => !x.IsGenericType && x.IsSubclassOf(typeof(Entity))).ToArray();
+        entities.SortBy(x => x.Name);
+
+        EntityTypeBox.ItemsSource = entities;
+    }
+
+    private bool EventTypeHasEntityType(EventType eventType)
+    {
+        switch (eventType)
+        {
+            case Comal.Classes.EventType.AfterSave:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    private void UpdateEventData()
+    {
+        if (EventType is null || (EventTypeHasEntityType(EventType.Value) && EntityType is null))
+        {
+            Data = null;
+            return;
+        }
+
+        Type eventType;
+        Type dataModelType;
+        switch (EventType.Value)
+        {
+            case Comal.Classes.EventType.AfterSave:
+                eventType = typeof(SaveEvent<>).MakeGenericType(EntityType!);
+                dataModelType = typeof(SaveEventDataModel<>).MakeGenericType(EntityType!);
+                break;
+            default:
+                Data = null;
+                return;
+        }
+
+        var ev = Activator.CreateInstance(eventType);
+        EventDataType = eventType;
+        DataModelType = dataModelType;
+        Data = (Activator.CreateInstance(typeof(EventData<,>).MakeGenericType(eventType, dataModelType), ev) as IEventData)!;
+    }
+
+    public static bool Run(ref EventType evType, ref IEventData? eventData)
+    {
+        var editor = new EventEditor();
+
+        editor.EventType = evType;
+        editor.Data = eventData;
+
+        var dialog = new DynamicContentDialog(editor)
+        {
+            SizeToContent = SizeToContent.Height,
+            Title = "Edit Event"
+        };
+
+        if(dialog.ShowDialog() == true)
+        {
+            return true;
+        }
+        else
+        {
+            return false;
+        }
+    }
+}

+ 89 - 0
prs.shared/Grids/EventEditor/EventEditorControl.cs

@@ -0,0 +1,89 @@
+using InABox.WPF;
+using PRS.Shared.Events;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+
+namespace PRS.Shared;
+
+public class EventEditorControl<TEvent, TDataModel> : Grid
+    where TEvent : IEvent<TDataModel>
+    where TDataModel : IEventDataModel
+{
+    public EventData<TEvent, TDataModel> Data { get; set; }
+
+    public EventEditorControl(EventData<TEvent, TDataModel> evData)
+    {
+        Data = evData;
+
+        var triggerLabelBorder = new Border
+        {
+            BorderBrush = Colors.Gray.ToBrush(),
+            BorderThickness = new(0.75),
+            Background = Colors.WhiteSmoke.ToBrush(),
+            Child = new Label
+            {
+                Content = "Triggers",
+                VerticalAlignment = System.Windows.VerticalAlignment.Center,
+                HorizontalAlignment = System.Windows.HorizontalAlignment.Center
+            }
+        };
+
+        var actionsLabelBorder = new Border
+        {
+            BorderBrush = Colors.Gray.ToBrush(),
+            BorderThickness = new(0.75),
+            Background = Colors.WhiteSmoke.ToBrush(),
+            Child = new Label
+            {
+                Content = "Actions",
+                VerticalAlignment = System.Windows.VerticalAlignment.Center,
+                HorizontalAlignment = System.Windows.HorizontalAlignment.Center
+            }
+        };
+
+        var separator = new StackPanel
+        {
+            Orientation = Orientation.Horizontal,
+        };
+        separator.Children.Add(new Separator
+        {
+            Style = (Style)Application.Current.TryFindResource(ToolBar.SeparatorStyleKey),
+            Margin = new(3, 0, 3, 0)
+        });
+
+        var triggersGrid = new EventTriggerGrid<TEvent, TDataModel>(Data.Triggers)
+        {
+            Margin = new(0, 5, 0, 0)
+        };
+        var actionsControl = new ContentControl
+        {
+            Margin = new(0, 5, 0, 0),
+            Content = new Border
+            {
+                BorderBrush = Colors.Gray.ToBrush(),
+                BorderThickness = new(0.75),
+                Background = Colors.White.ToBrush()
+            }
+        };
+
+        this.AddRow(GridUnitType.Auto);
+        this.AddRow(GridUnitType.Star);
+        this.AddColumn(GridUnitType.Star);
+        this.AddColumn(GridUnitType.Auto);
+        this.AddColumn(GridUnitType.Star);
+
+        this.AddChild(triggerLabelBorder, 0, 0);
+        this.AddChild(separator, 0, 1, rowSpan: 2);
+        this.AddChild(actionsLabelBorder, 0, 2);
+        this.AddChild(triggersGrid, 1, 0);
+        this.AddChild(actionsControl, 1, 2);
+
+        triggersGrid.Refresh(true, true);
+    }
+}

+ 84 - 0
prs.shared/Grids/EventEditor/EventTriggerGrid.cs

@@ -0,0 +1,84 @@
+using InABox.Core;
+using InABox.DynamicGrid;
+using InABox.WPF;
+using PRS.Shared.Events;
+using PRS.Shared.Grids.EventEditor;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Controls;
+using System.Windows.Media;
+
+namespace PRS.Shared;
+
+public class EventTriggerContainer<TEvent, TDataModel> : BaseObject
+    where TEvent : IEvent<TDataModel>
+    where TDataModel : IEventDataModel
+{
+    public IEventTrigger<TEvent, TDataModel> Trigger { get; set; }
+
+    public string TriggerType => Trigger.GetType().GetCaption();
+
+    public string Description => Trigger.GetDescription();
+}
+
+public class EventTriggerGrid<TEvent, TDataModel> : DynamicItemsListGrid<EventTriggerContainer<TEvent, TDataModel>>
+    where TEvent : IEvent<TDataModel>
+    where TDataModel : IEventDataModel
+{
+    public IEnumerable<IEventTrigger<TEvent, TDataModel>> EventTriggers => Items.Select(x => x.Trigger);
+
+    public EventTriggerGrid(IEnumerable<IEventTrigger<TEvent, TDataModel>> items)
+    {
+        Items.AddRange(items.Select(x => new EventTriggerContainer<TEvent, TDataModel> { Trigger = x }));
+    }
+
+    protected override void DoReconfigure(DynamicGridOptions options)
+    {
+        base.DoReconfigure(options);
+        options.AddRows = true;
+        options.EditRows = true;
+        options.DeleteRows = true;
+    }
+
+    public override DynamicGridColumns GenerateColumns()
+    {
+        var cols = new DynamicGridColumns<EventTriggerContainer<TEvent, TDataModel>>();
+        cols.Add(x => x.Description);
+        return cols;
+    }
+
+    protected override void DoAdd(bool openEditorOnDirectEdit = false)
+    {
+        var types = EventUtils.GetEventTriggerTypes(typeof(TEvent));
+
+        var menu = new ContextMenu();
+        foreach(var type in types)
+        {
+            menu.AddItem(type.GetCaption(), null, type, MenuAdd_Click);
+        }
+        menu.IsOpen = true;
+    }
+
+    private void MenuAdd_Click(Type type)
+    {
+        if (type.IsGenericType)
+        {
+            type = type.MakeGenericType(typeof(TEvent).GenericTypeArguments);
+        }
+        var trigger = (Activator.CreateInstance(type) as IEventTrigger<TEvent, TDataModel>)!;
+        EditTrigger(trigger);
+
+        Items.Add(new() { Trigger = trigger });
+
+        Refresh(false, true);
+    }
+
+    private void EditTrigger(IEventTrigger<TEvent, TDataModel> trigger)
+    {
+        EventTriggerEditors.EditTrigger(trigger);
+    }
+}
+

+ 50 - 0
prs.shared/Grids/EventGrid.cs

@@ -3,8 +3,10 @@ using InABox.Clients;
 using InABox.Core;
 using InABox.DynamicGrid;
 using InABox.WPF;
+using PRS.Shared.Events;
 using System;
 using System.Collections.Generic;
+using System.IO;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
@@ -29,6 +31,8 @@ public class EventGrid : DynamicDataGrid<Event>
         ActionColumns.Add(new DynamicMenuColumn(BuildMenu));
     }
 
+    #region Action Columns
+
     private FrameworkElement? Subscribed_ToolTip(DynamicActionColumn column, CoreRow? row)
     {
         if(row is null)
@@ -103,6 +107,52 @@ public class EventGrid : DynamicDataGrid<Event>
         InvalidateRow(row);
     }
 
+    #endregion
+
+    private readonly Column<Event> EventTypeColumn = new(x => x.EventType);
+
+    protected override void CustomiseEditor(Event[] items, DynamicGridColumn column, BaseEditor editor)
+    {
+        base.CustomiseEditor(items, column, editor);
+
+        if(EventTypeColumn.IsEqualTo(column.ColumnName) && editor is EnumLookupEditor enumEditor)
+        {
+            var ev = items.First();
+
+            var editButton = new EditorButton(ev, "Edit Event", 100, EditEvent_Click, false);
+            enumEditor.Buttons = [editButton];
+        }
+    }
+
+    private void EditEvent_Click(object editor, object? item)
+    {
+        if (item is not Event ev) return;
+
+        var type = ev.EventType;
+
+        IEventData? data = null;
+        if(ev.Data.Length != 0)
+        {
+            using var stream = new MemoryStream(ev.Data);
+            var reader = new CoreBinaryReader(stream, BinarySerializationSettings.Latest);
+            data = EventUtils.Deserialize(reader);
+        }
+        if(EventEditor.Run(ref type, ref data))
+        {
+            ev.EventType = type;
+            if(data is not null)
+            {
+                using var stream = new MemoryStream();
+                EventUtils.Serialize(data, new CoreBinaryWriter(stream, BinarySerializationSettings.Latest));
+                ev.Data = stream.ToArray();
+            }
+            else
+            {
+                ev.Data = [];
+            }
+        }
+    }
+
     protected override void Reload(Filters<Event> criteria, Columns<Event> columns, ref SortOrder<Event>? sort, CancellationToken token, Action<CoreTable?, Exception?> action)
     {
         base.Reload(criteria, columns, ref sort, token, (data, error) =>