lintx 2 сар өмнө
parent
commit
46fe68ebef
41 өөрчлөгдсөн 1207 нэмэгдсэн , 0 устгасан
  1. 34 0
      Demo.sln
  2. 9 0
      FakeLibrary/Fake.cs
  3. 9 0
      FakeLibrary/FakeLibrary.csproj
  4. BIN
      Waaagh/App.sqlite
  5. 13 0
      Waaagh/AppDatabaseContext.cs
  6. 10 0
      Waaagh/AssemblyInfo.cs
  7. 41 0
      Waaagh/Converters/DefaultItemsVisibilityConverter.cs
  8. 30 0
      Waaagh/Converters/IsCollapsedConverter.cs
  9. 40 0
      Waaagh/CustomControl/WaaaghWindow.cs
  10. 19 0
      Waaagh/Helpers/ModelUpdateHelper.cs
  11. 13 0
      Waaagh/Helpers/WindowHelper.cs
  12. 78 0
      Waaagh/Migrations/20241117103737_InitialCreate.Designer.cs
  13. 49 0
      Waaagh/Migrations/20241117103737_InitialCreate.cs
  14. 75 0
      Waaagh/Migrations/AppDatabaseContextModelSnapshot.cs
  15. 37 0
      Waaagh/Models/Message.cs
  16. 25 0
      Waaagh/Models/Scenario.cs
  17. 92 0
      Waaagh/Program.cs
  18. 15 0
      Waaagh/Services/Abstract/IMessageCatcher.cs
  19. 16 0
      Waaagh/Services/Abstract/IMessageManager.cs
  20. 16 0
      Waaagh/Services/Abstract/IScenarioManager.cs
  21. 7 0
      Waaagh/Services/MessageCatcher.cs
  22. 34 0
      Waaagh/Services/MessageManager.cs
  23. 86 0
      Waaagh/Services/ScenarioManager.cs
  24. 4 0
      Waaagh/Services/ScenarioRunner.cs
  25. 103 0
      Waaagh/Themes/CustomControl/WaaaghWindow.xaml
  26. 10 0
      Waaagh/Themes/Generic.xaml
  27. 8 0
      Waaagh/ViewModels/MainViewModel.cs
  28. 37 0
      Waaagh/Views/MainWindow.xaml
  29. 9 0
      Waaagh/Views/MainWindow.xaml.cs
  30. 49 0
      Waaagh/Waaagh.csproj
  31. 6 0
      Waaagh/appsettings.json
  32. 9 0
      WpfSample/App.xaml
  33. 7 0
      WpfSample/App.xaml.cs
  34. 10 0
      WpfSample/AssemblyInfo.cs
  35. 25 0
      WpfSample/AttachedProperty.cs
  36. 54 0
      WpfSample/MainWindow.xaml
  37. 34 0
      WpfSample/MainWindow.xaml.cs
  38. 34 0
      WpfSample/ViewModels/DemoGroupViewModel.cs
  39. 15 0
      WpfSample/ViewModels/DemoItemViewModel.cs
  40. 30 0
      WpfSample/ViewModels/MainViewModel.cs
  41. 15 0
      WpfSample/WpfSample.csproj

+ 34 - 0
Demo.sln

@@ -0,0 +1,34 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.12.35506.116
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FakeLibrary", "FakeLibrary\FakeLibrary.csproj", "{3843140D-88B6-44C3-8D95-4B4465738AF9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WpfSample", "WpfSample\WpfSample.csproj", "{247C091F-725D-4B0F-80DA-9738B159B179}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Waaagh", "Waaagh\Waaagh.csproj", "{AF5669D4-D735-4ABD-B4A3-CC88DDF77137}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{3843140D-88B6-44C3-8D95-4B4465738AF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{3843140D-88B6-44C3-8D95-4B4465738AF9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{3843140D-88B6-44C3-8D95-4B4465738AF9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{3843140D-88B6-44C3-8D95-4B4465738AF9}.Release|Any CPU.Build.0 = Release|Any CPU
+		{247C091F-725D-4B0F-80DA-9738B159B179}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{247C091F-725D-4B0F-80DA-9738B159B179}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{247C091F-725D-4B0F-80DA-9738B159B179}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{247C091F-725D-4B0F-80DA-9738B159B179}.Release|Any CPU.Build.0 = Release|Any CPU
+		{AF5669D4-D735-4ABD-B4A3-CC88DDF77137}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{AF5669D4-D735-4ABD-B4A3-CC88DDF77137}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{AF5669D4-D735-4ABD-B4A3-CC88DDF77137}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{AF5669D4-D735-4ABD-B4A3-CC88DDF77137}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+EndGlobal

+ 9 - 0
FakeLibrary/Fake.cs

@@ -0,0 +1,9 @@
+namespace FakeLibrary {
+    public class SECSApi {
+
+    }
+
+    public class SECSValue {
+
+    }
+}

+ 9 - 0
FakeLibrary/FakeLibrary.csproj

@@ -0,0 +1,9 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net8.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+</Project>

BIN
Waaagh/App.sqlite


+ 13 - 0
Waaagh/AppDatabaseContext.cs

@@ -0,0 +1,13 @@
+using Microsoft.EntityFrameworkCore;
+using Waaagh.Models;
+
+namespace Waaagh {
+    public class AppDatabaseContext: DbContext {
+        public AppDatabaseContext(DbContextOptions<AppDatabaseContext> options) : base(options) {
+
+        }
+        public DbSet<Message> Messages { get; set; }
+        public DbSet<Message> Templates { get; set; }
+        public DbSet<Scenario> Scenarios { get; set; }
+    }
+}

+ 10 - 0
Waaagh/AssemblyInfo.cs

@@ -0,0 +1,10 @@
+using System.Windows;
+
+[assembly: ThemeInfo(
+    ResourceDictionaryLocation.None,            //where theme specific resource dictionaries are located
+                                                //(used if a resource is not found in the page,
+                                                // or application resource dictionaries)
+    ResourceDictionaryLocation.SourceAssembly   //where the generic resource dictionary is located
+                                                //(used if a resource is not found in the page,
+                                                // app, or any theme specific resource dictionaries)
+)]

+ 41 - 0
Waaagh/Converters/DefaultItemsVisibilityConverter.cs

@@ -0,0 +1,41 @@
+using System.Collections;
+using System.Globalization;
+using System.Windows;
+using System.Windows.Data;
+using System.Windows.Markup;
+
+namespace Waaagh.Converters {
+    public class DefaultContentVisibilityConverter: MarkupExtension, IValueConverter {
+        public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
+            // 默认使用 Hidden 作为不显示
+            Visibility unvisibility = Visibility.Hidden;
+            if (parameter is Visibility mode) {
+                unvisibility = mode;
+            }
+            else if (parameter is bool isCollapsed && isCollapsed) {
+                unvisibility = Visibility.Collapsed;
+            }
+            else if (parameter is string str && bool.TryParse(str, out isCollapsed) && isCollapsed) {
+                unvisibility = Visibility.Collapsed;
+            }
+
+            // 任意类型为空, 显示默认内容
+            if (value == null) {
+                return Visibility.Visible;
+            }
+            // 集合类型且空集合, 显示默认内容
+            if (value is ICollection collection && collection.Count == 0) {
+                return Visibility.Visible;
+            }
+            return unvisibility;
+        }
+
+        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
+            throw new NotImplementedException();
+        }
+
+        public override object ProvideValue(IServiceProvider serviceProvider) {
+            return this;
+        }
+    }
+}

+ 30 - 0
Waaagh/Converters/IsCollapsedConverter.cs

@@ -0,0 +1,30 @@
+using System.Globalization;
+using System.Windows;
+using System.Windows.Data;
+using System.Windows.Markup;
+
+namespace Waaagh.Converters {
+    internal class IsCollapsedConverter: MarkupExtension, IValueConverter {
+        public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
+            if (value == null || parameter == null) {
+                return Visibility.Visible;
+            }
+            if (value.GetType().Equals(parameter.GetType()) == false) {
+                return Visibility.Visible;
+            }
+            if (value.Equals(parameter) == false) {
+                return Visibility.Visible;
+            }
+
+            return Visibility.Collapsed;
+        }
+
+        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
+            throw new NotImplementedException();
+        }
+
+        public override object ProvideValue(IServiceProvider serviceProvider) {
+            return this;
+        }
+    }
+}

+ 40 - 0
Waaagh/CustomControl/WaaaghWindow.cs

@@ -0,0 +1,40 @@
+using System.Windows;
+using System.Windows.Input;
+
+namespace Waaagh.CustomControl {
+    public class WaaaghWindow: Window {
+        #region Static
+        static WaaaghWindow() {
+            DefaultStyleKeyProperty.OverrideMetadata(typeof(WaaaghWindow), new FrameworkPropertyMetadata(typeof(WaaaghWindow)));
+        }
+
+        public static readonly DependencyProperty TitleBarContentProperty = DependencyProperty.Register(
+            "TitleBarContent", typeof(object), typeof(WaaaghWindow),
+            new PropertyMetadata(null));
+
+        public static readonly DependencyProperty TitleBarButtonsProperty = DependencyProperty.Register(
+            "TitleBarButtons", typeof(List<object>), typeof(WaaaghWindow),
+            new PropertyMetadata(new List<object>()));
+
+        #endregion
+
+        public WaaaghWindow() {
+            DefaultStyleKey = typeof(WaaaghWindow);
+            CommandBindings.Add(new CommandBinding(SystemCommands.CloseWindowCommand, (s, e) => { SystemCommands.CloseWindow(this); }));
+            CommandBindings.Add(new CommandBinding(SystemCommands.MinimizeWindowCommand, (s, e) => { SystemCommands.MinimizeWindow(this); }));
+            CommandBindings.Add(new CommandBinding(SystemCommands.MaximizeWindowCommand, (s, e) => { SystemCommands.MaximizeWindow(this); }));
+            CommandBindings.Add(new CommandBinding(SystemCommands.RestoreWindowCommand, (s, e) => { SystemCommands.RestoreWindow(this); }));
+        }
+
+        public object TitleBarContent {
+            get { return (object)GetValue(TitleBarContentProperty); }
+            set { SetValue(TitleBarContentProperty, value); }
+        }
+
+        public List<object> TitleBarButtons {
+            get { return (List<object>)GetValue(TitleBarButtonsProperty); }
+            set { SetValue(TitleBarButtonsProperty, value); }
+        }
+
+    }
+}

+ 19 - 0
Waaagh/Helpers/ModelUpdateHelper.cs

@@ -0,0 +1,19 @@
+using System.Reflection;
+
+namespace Waaagh.Helpers {
+    public interface IUpdateReferences<T> {
+        void UpdateReferences(T value);
+    }
+
+    static internal class ModelUpdateHelper {
+        static public void UpdateWritableProperies<T>(ref T source, T value) where T : IUpdateReferences<T> {
+            Type type = typeof(T);
+            PropertyInfo[] properties = type.GetProperties();
+            foreach (PropertyInfo property in properties) {
+                if (property.CanWrite) {
+                    property.SetValue(source, property.GetValue(value));
+                }
+            }
+        }
+    }
+}

+ 13 - 0
Waaagh/Helpers/WindowHelper.cs

@@ -0,0 +1,13 @@
+using System.Windows;
+
+namespace Waaagh.Helpers {
+    public class WindowHelper {
+        public static Thickness ChromeThickness {
+            get {
+                var w = (SystemParameters.MaximizedPrimaryScreenWidth - SystemParameters.WorkArea.Width) / 2;
+                var h = (SystemParameters.MaximizedPrimaryScreenHeight - SystemParameters.WorkArea.Height) / 2;
+                return new Thickness(w + 1, h, w + 1, h);
+            }
+        }
+    }
+}

+ 78 - 0
Waaagh/Migrations/20241117103737_InitialCreate.Designer.cs

@@ -0,0 +1,78 @@
+// <auto-generated />
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Waaagh;
+
+#nullable disable
+
+namespace Waaagh.Migrations
+{
+    [DbContext(typeof(AppDatabaseContext))]
+    [Migration("20241117103737_InitialCreate")]
+    partial class InitialCreate
+    {
+        /// <inheritdoc />
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder.HasAnnotation("ProductVersion", "9.0.0");
+
+            modelBuilder.Entity("Waaagh.Models.Message", b =>
+                {
+                    b.Property<int>("Key")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<short>("Channel")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Content")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("Reply")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Key");
+
+                    b.ToTable("Message");
+                });
+
+            modelBuilder.Entity("Waaagh.Models.Scenario", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Description")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("IsLegacy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime>("LastUpdateTime")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Version")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("Scenarios");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}

+ 49 - 0
Waaagh/Migrations/20241117103737_InitialCreate.cs

@@ -0,0 +1,49 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Waaagh.Migrations {
+    /// <inheritdoc />
+    public partial class InitialCreate: Migration {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder) {
+            migrationBuilder.CreateTable(
+                name: "Message",
+                columns: table => new {
+                    Key = table.Column<int>(type: "INTEGER", nullable: false)
+                        .Annotation("Sqlite:Autoincrement", true),
+                    Channel = table.Column<short>(type: "INTEGER", nullable: false),
+                    Reply = table.Column<bool>(type: "INTEGER", nullable: false),
+                    Name = table.Column<string>(type: "TEXT", nullable: false),
+                    Content = table.Column<string>(type: "TEXT", nullable: false)
+                },
+                constraints: table => {
+                    table.PrimaryKey("PK_Message", x => x.Key);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "Scenarios",
+                columns: table => new {
+                    Id = table.Column<int>(type: "INTEGER", nullable: false)
+                        .Annotation("Sqlite:Autoincrement", true),
+                    Name = table.Column<string>(type: "TEXT", nullable: false),
+                    Description = table.Column<string>(type: "TEXT", nullable: false),
+                    LastUpdateTime = table.Column<DateTime>(type: "TEXT", nullable: false),
+                    Version = table.Column<int>(type: "INTEGER", nullable: false),
+                    IsLegacy = table.Column<bool>(type: "INTEGER", nullable: false)
+                },
+                constraints: table => {
+                    table.PrimaryKey("PK_Scenarios", x => x.Id);
+                });
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder) {
+            migrationBuilder.DropTable(
+                name: "Message");
+
+            migrationBuilder.DropTable(
+                name: "Scenarios");
+        }
+    }
+}

+ 75 - 0
Waaagh/Migrations/AppDatabaseContextModelSnapshot.cs

@@ -0,0 +1,75 @@
+// <auto-generated />
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Waaagh;
+
+#nullable disable
+
+namespace Waaagh.Migrations
+{
+    [DbContext(typeof(AppDatabaseContext))]
+    partial class AppDatabaseContextModelSnapshot : ModelSnapshot
+    {
+        protected override void BuildModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder.HasAnnotation("ProductVersion", "9.0.0");
+
+            modelBuilder.Entity("Waaagh.Models.Message", b =>
+                {
+                    b.Property<int>("Key")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<short>("Channel")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Content")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("Reply")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Key");
+
+                    b.ToTable("Message");
+                });
+
+            modelBuilder.Entity("Waaagh.Models.Scenario", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Description")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("IsLegacy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime>("LastUpdateTime")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Version")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("Scenarios");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}

+ 37 - 0
Waaagh/Models/Message.cs

@@ -0,0 +1,37 @@
+using System.ComponentModel.DataAnnotations;
+using Waaagh.Helpers;
+
+namespace Waaagh.Models {
+    public class Message: IUpdateReferences<Message> {
+
+        [Key]
+        public int Key { get; set; }
+        public short Channel { get; set; } = 0x0000;
+        public bool Reply { get; set; } = false;
+        public string Name { get; set; } = string.Empty;
+
+        private string content = string.Empty;
+        public string Content {
+            get => content;
+            set {
+                content = value;
+                // Value = SECSValue.Parse(content);
+                //if (Value == null) {
+                //    throw new ArgumentException($"{nameof(Message)}: Content Parsing Failed.");
+                //}
+            }
+        }
+
+        #region Serialize Ignore
+        public int Stream => (Channel >> 8) & 0xff;
+        public int Function => (Channel >> 0) & 0xff;
+        //public SECSValue? Value { get; private set; } = default;
+        #endregion
+
+        public void UpdateReferences(Message value) {
+            Message message = this;
+            ModelUpdateHelper.UpdateWritableProperies(ref message, value);
+        }
+
+    }
+}

+ 25 - 0
Waaagh/Models/Scenario.cs

@@ -0,0 +1,25 @@
+using Waaagh.Helpers;
+
+namespace Waaagh.Models {
+    public class Scenario: IUpdateReferences<Scenario> {
+        public int Id { get; set; } = 0;
+        public string Name { get; set; } = string.Empty;
+        public string Description { get; set; } = string.Empty;
+
+        public DateTime LastUpdateTime { get; set; } = DateTime.MinValue;
+        public int Version { get; set; } = 0;
+
+        public Scenario() { }
+
+
+
+        public void UpdateReferences(Scenario value) {
+            Scenario scenario = this;
+            ModelUpdateHelper.UpdateWritableProperies(ref scenario, value);
+        }
+
+        #region Falgs
+        public bool IsLegacy { get; set; }
+        #endregion
+    }
+}

+ 92 - 0
Waaagh/Program.cs

@@ -0,0 +1,92 @@
+using System.Windows;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Waaagh.Models;
+using Waaagh.Services.Abstract;
+using Waaagh.ViewModels;
+using Waaagh.Views;
+
+namespace Waaagh {
+    public class Program {
+        [STAThread]
+        static public void Main(string[] args) {
+            SQLitePCL.Batteries.Init();
+
+            IHost host = CreateHostBuilder(args).Build();
+            host.Start();
+
+            Application application = host.Services.GetRequiredService<Application>();
+
+#if DEBUG
+            //  DbTest(host);
+#endif
+
+            application.Run();
+        }
+
+        static private IHostBuilder CreateHostBuilder(string[] args) {
+            return Host.CreateDefaultBuilder(args)
+                .ConfigureServices(services => {
+                    services.AddDbContextFactory<AppDatabaseContext>((sp, options) => {
+                        IConfiguration configuration = sp.GetRequiredService<IConfiguration>();
+                        string? sqliteConnectionString = configuration.GetConnectionString("Default");
+                        if (string.IsNullOrWhiteSpace(sqliteConnectionString)) {
+                            throw new Exception("Configuration Database Connection String Error.");
+                        }
+                        options.UseSqlite(sqliteConnectionString);
+                    });
+                })
+                .AddDefaultMessageManager()
+                .AddDefaultMessageCatcher()
+                .AddDefaultScenarioManager()
+                .ConfigureServices(services => {
+                    // MainWindow
+                    services.AddSingleton<MainViewModel>();
+                    services.AddSingleton<MainWindow>(sp => {
+                        return new MainWindow() {
+                            DataContext = sp.GetRequiredService<MainViewModel>()
+                        };
+                    });
+                })
+                .ConfigureServices(services => {
+                    // Application
+                    services.AddSingleton<Application>(sp => {
+                        Application application = new Application() {
+                            MainWindow = sp.GetRequiredService<MainWindow>(),
+                        };
+                        application.Startup += (s, e) => {
+                            if (s is Application application) {
+                                application.MainWindow.Show();
+                            }
+                        };
+                        return application;
+                    });
+                });
+        }
+
+        static public void DbTest(IHost host) {
+            IDbContextFactory<AppDatabaseContext> factory = host.Services.GetRequiredService<IDbContextFactory<AppDatabaseContext>>();
+            AppDatabaseContext context = factory.CreateDbContext();
+            {
+                Scenario scenario = new Scenario() {
+                    Id = 1,
+                    Name = "Test"
+                };
+                if (context.Scenarios.Contains(scenario) == false) {
+                    context.Scenarios.Add(scenario);
+
+                }
+                else {
+                    scenario.IsLegacy = true;
+                    context.Scenarios.Update(scenario);
+                }
+                context.SaveChanges();
+            }
+            foreach (Scenario scenario in context.Scenarios) {
+                Console.WriteLine($"{scenario.Name}-{scenario.Id}<{scenario.IsLegacy}>: {scenario.Description}.");
+            }
+        }
+    }
+}

+ 15 - 0
Waaagh/Services/Abstract/IMessageCatcher.cs

@@ -0,0 +1,15 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace Waaagh.Services.Abstract {
+    public interface IMessageCatcher {
+
+    }
+    static public partial class IHostBuilderExtensions {
+        static public IHostBuilder AddDefaultMessageCatcher(this IHostBuilder builder) {
+            return builder.ConfigureServices((services) => {
+                services.AddSingleton<IMessageCatcher, MessageCatcher>();
+            });
+        }
+    }
+}

+ 16 - 0
Waaagh/Services/Abstract/IMessageManager.cs

@@ -0,0 +1,16 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace Waaagh.Services.Abstract {
+    public interface IMessageManager {
+
+    }
+
+    static public partial class IHostBuilderExtensions {
+        static public IHostBuilder AddDefaultMessageManager(this IHostBuilder builder) {
+            return builder.ConfigureServices((services) => {
+                services.AddSingleton<IMessageManager, MessageManager>();
+            });
+        }
+    }
+}

+ 16 - 0
Waaagh/Services/Abstract/IScenarioManager.cs

@@ -0,0 +1,16 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace Waaagh.Services.Abstract {
+    public interface IScenarioManager {
+
+    }
+
+    static public partial class IHostBuilderExtensions {
+        static public IHostBuilder AddDefaultScenarioManager(this IHostBuilder builder) {
+            return builder.ConfigureServices((services) => {
+                services.AddSingleton<IScenarioManager, ScenarioManager>();
+            });
+        }
+    }
+}

+ 7 - 0
Waaagh/Services/MessageCatcher.cs

@@ -0,0 +1,7 @@
+using Waaagh.Services.Abstract;
+
+namespace Waaagh.Services {
+    internal class MessageCatcher: IMessageCatcher {
+
+    }
+}

+ 34 - 0
Waaagh/Services/MessageManager.cs

@@ -0,0 +1,34 @@
+using Microsoft.EntityFrameworkCore;
+using Waaagh.Models;
+using Waaagh.Services.Abstract;
+
+namespace Waaagh.Services {
+    internal class MessageManager: IMessageManager {
+
+        private IDbContextFactory<AppDatabaseContext> DbContextFactory { get; }
+
+        public MessageManager(IDbContextFactory<AppDatabaseContext> dbContextFactory) {
+            DbContextFactory = dbContextFactory;
+            Templates = new Dictionary<string, Message>();
+            Messages = new Dictionary<string, Message>();
+        }
+
+        private Dictionary<string, Message> Templates { get; set; }
+        private Dictionary<string, Message> Messages { get; set; }
+
+
+
+        public void SaveMessagesAsync() {
+
+        }
+
+        public void LoadMessages() {
+            AppDatabaseContext dbContext = DbContextFactory.CreateDbContext();
+            foreach (Message message in dbContext.Messages) {
+                if (Messages.TryAdd(message.Name, message) == false) {
+                    Messages[message.Name].UpdateReferences(message);
+                }
+            }
+        }
+    }
+}

+ 86 - 0
Waaagh/Services/ScenarioManager.cs

@@ -0,0 +1,86 @@
+using Microsoft.EntityFrameworkCore;
+using Waaagh.Models;
+using Waaagh.Services.Abstract;
+
+namespace Waaagh.Services {
+    internal class ScenarioManager: IScenarioManager {
+        private readonly IDbContextFactory<AppDatabaseContext> AppDatabaseContextFactory;
+
+        public ScenarioManager(IDbContextFactory<AppDatabaseContext> dbContextFactory) {
+            AppDatabaseContextFactory = dbContextFactory;
+
+            using AppDatabaseContext dbContext = AppDatabaseContextFactory.CreateDbContext();
+            foreach (Scenario scenario in dbContext.Scenarios) {
+
+            }
+
+        }
+
+        private Dictionary<string, Scenario> Scenarios { get; }
+
+        #region IScenarioManager
+        public bool AddScenario(Scenario scenario) {
+            string name = scenario.Name;
+            if (Scenarios.TryAdd(name, scenario) == false) {
+                Scenarios[name].UpdateReferences(scenario);
+            }
+            return true;
+        }
+
+        public bool TryAddScenario(Scenario scenario) {
+            string name = scenario.Name;
+            return Scenarios.TryAdd(name, scenario);
+        }
+
+        public bool RemoveScenario(string name) {
+            return Scenarios.Remove(name);
+        }
+
+        public int SaveAll() {
+            int count = 0;
+
+            AppDatabaseContext dbContext = AppDatabaseContextFactory.CreateDbContext();
+
+            int toSaveIndex = 0;
+            List<Scenario> toSaveList = Scenarios.Values.ToList();
+            foreach (Scenario scenario in dbContext.Scenarios) {
+                while (toSaveList[toSaveIndex].IsLegacy == true) {
+                    ++toSaveIndex;
+                }
+                Scenario toSaveScenario = toSaveList[toSaveIndex];
+                if (toSaveScenario.Id == scenario.Id) {
+                    // Update 
+                }
+                else {
+                    // Insert
+                }
+            }
+
+            dbContext.SaveChanges();
+            return count;
+        }
+
+        public int ReloadAll() {
+            int legacyCount = 0;
+            foreach (Scenario scenario in Scenarios.Values) {
+                scenario.IsLegacy = true;
+                ++legacyCount;
+            }
+
+            AppDatabaseContext dbContext = AppDatabaseContextFactory.CreateDbContext();
+            foreach (Scenario scenario in dbContext.Scenarios) {
+                scenario.IsLegacy = false;
+                string name = scenario.Name;
+                if (Scenarios.ContainsKey(name) == true) {
+                    Scenarios[name].UpdateReferences(scenario);
+                }
+                else {
+                    Scenarios.Add(name, scenario);
+                }
+            }
+            return Scenarios.Count() - legacyCount;
+        }
+
+        #endregion
+    }
+}

+ 4 - 0
Waaagh/Services/ScenarioRunner.cs

@@ -0,0 +1,4 @@
+namespace Waaagh.Services {
+    internal class ScenarioRunner {
+    }
+}

+ 103 - 0
Waaagh/Themes/CustomControl/WaaaghWindow.xaml

@@ -0,0 +1,103 @@
+<ResourceDictionary
+    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:cnvt="clr-namespace:Waaagh.Converters"
+    xmlns:custom="clr-namespace:Waaagh.CustomControl"
+    xmlns:helper="clr-namespace:Waaagh.Helpers">
+
+    <Style x:Key="TitleButtonStyle" TargetType="Button">
+        <Setter Property="Margin" Value="1"/>
+        <Setter Property="BorderThickness" Value="0"/>
+        <Setter Property="Width" Value="28"/>
+        <Setter Property="Height" Value="28"/>
+        <Setter Property="HorizontalContentAlignment" Value="Center"/>
+        <Setter Property="VerticalContentAlignment" Value="Center"/>
+    </Style>
+
+    <TextBlock x:Key="TestContent" Text="Test"/>
+    <TextBlock x:Key="TestContent2" Text="Testsds"/>
+
+    <Style TargetType="{x:Type custom:WaaaghWindow}">
+
+        <Setter Property="WindowChrome.WindowChrome">
+            <Setter.Value>
+                <WindowChrome CaptionHeight="32" ResizeBorderThickness="5" UseAeroCaptionButtons="False"/>
+            </Setter.Value>
+        </Setter>
+
+        <Setter Property="Width" Value="450"/>
+        <Setter Property="Height" Value="800"/>
+        <Setter Property="Background" Value="AliceBlue"/>
+
+        <Setter Property="Template">
+            <Setter.Value>
+                <ControlTemplate TargetType="{x:Type custom:WaaaghWindow}">
+                    <Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
+                        <Border x:Name="LayoutRoot" BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}">
+                            <DockPanel>
+                                <Border DockPanel.Dock="Top" Height="28">
+                                    <DockPanel LastChildFill="True" Background="Beige">
+
+
+                                        <!--#region TitleButton-->
+                                        <Grid DockPanel.Dock="Right">
+                                            <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center" WindowChrome.IsHitTestVisibleInChrome="True"
+                                                Visibility="{TemplateBinding TitleBarButtons, Converter={cnvt:DefaultContentVisibilityConverter}}">
+                                                <Button Style="{StaticResource TitleButtonStyle}" Command="{x:Static SystemCommands.MinimizeWindowCommand}" Content="-"/>
+                                                <Button Style="{StaticResource TitleButtonStyle}" Command="{x:Static SystemCommands.MaximizeWindowCommand}" Content="{StaticResource TestContent}"
+                                                    Visibility="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=WindowState, Converter={cnvt:IsCollapsedConverter}, ConverterParameter={x:Static WindowState.Maximized}}"/>
+                                                <Button Style="{StaticResource TitleButtonStyle}" Command="{x:Static SystemCommands.RestoreWindowCommand}" Content="{StaticResource TestContent2}"
+                                                    Visibility="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=WindowState, Converter={cnvt:IsCollapsedConverter}, ConverterParameter={x:Static WindowState.Normal}}"/>
+                                                <Button Style="{StaticResource TitleButtonStyle}" Command="{x:Static SystemCommands.CloseWindowCommand}" Content="X"/>
+                                            </StackPanel>
+                                            <ItemsControl ItemsSource="{TemplateBinding TitleBarButtons}" HorizontalAlignment="Right" VerticalAlignment="Center">
+                                                <ItemsControl.ItemsPanel>
+                                                    <ItemsPanelTemplate>
+                                                        <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center" WindowChrome.IsHitTestVisibleInChrome="True"/>
+                                                    </ItemsPanelTemplate>
+                                                </ItemsControl.ItemsPanel>
+
+                                                <ItemsControl.ItemContainerStyle>
+                                                    <Style TargetType="Button" BasedOn="{StaticResource TitleButtonStyle}"/>
+                                                </ItemsControl.ItemContainerStyle>
+                                            </ItemsControl>
+                                        </Grid>
+                                        <!--#endregion-->
+
+                                        <!--#region TitleContent-->
+                                        <Border DockPanel.Dock="Left">
+                                            <Grid>
+                                                <StackPanel Orientation="Horizontal" Visibility="{TemplateBinding TitleBarContent, Converter={cnvt:DefaultContentVisibilityConverter}}">
+                                                    <Image Source="{TemplateBinding Icon}"/>
+                                                    <TextBlock Text="{TemplateBinding Title}" VerticalAlignment="Center"/>
+                                                </StackPanel>
+                                                <ContentPresenter ContentSource="TitleBarContent"/>
+                                            </Grid>
+                                        </Border>
+                                        <!--#endregion-->
+
+                                    </DockPanel>
+                                </Border>
+
+                                <Border Background="{TemplateBinding Background}">
+                                    <AdornerDecorator>
+                                        <ContentPresenter/>
+                                    </AdornerDecorator>
+                                </Border>
+
+                            </DockPanel>
+                        </Border>
+                    </Border>
+
+                    <ControlTemplate.Triggers>
+                        <Trigger Property="WindowState" Value="Maximized">
+                            <Setter TargetName="LayoutRoot" Property="Margin" Value="{x:Static helper:WindowHelper.ChromeThickness}"/>
+                        </Trigger>
+                    </ControlTemplate.Triggers>
+
+                </ControlTemplate>
+            </Setter.Value>
+        </Setter>
+    </Style>
+</ResourceDictionary>

+ 10 - 0
Waaagh/Themes/Generic.xaml

@@ -0,0 +1,10 @@
+<ResourceDictionary
+    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+    xmlns:local="clr-namespace:Waaagh">
+
+    <ResourceDictionary.MergedDictionaries>
+        <ResourceDictionary Source="Waaagh;component/Themes/CustomControl/WaaaghWindow.xaml"/>
+    </ResourceDictionary.MergedDictionaries>
+
+</ResourceDictionary>

+ 8 - 0
Waaagh/ViewModels/MainViewModel.cs

@@ -0,0 +1,8 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace Waaagh.ViewModels {
+    internal partial class MainViewModel: ObservableObject {
+        [ObservableProperty]
+        private string greeting = nameof(Greeting);
+    }
+}

+ 37 - 0
Waaagh/Views/MainWindow.xaml

@@ -0,0 +1,37 @@
+<custom:WaaaghWindow x:Class="Waaagh.Views.MainWindow"
+        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:vm="clr-namespace:Waaagh.ViewModels"
+        xmlns:custom="clr-namespace:Waaagh.CustomControl"
+        mc:Ignorable="d"
+        Icon="C:\Users\L87\Desktop\touhou\琪露诺3.jpg"
+        Title="MainWindow" Height="450" Width="800"
+        d:DataContext="{d:DesignInstance vm:MainViewModel}" WindowStyle="SingleBorderWindow">
+
+    <!--<custom:WaaaghWindow.TitleBarContent>
+        <StackPanel Orientation="Horizontal">
+            <Image Source="C:\Users\L87\Desktop\touhou\山城高岭.jpg"/>
+            <TextBlock Text="山城高岭"/>
+        </StackPanel>
+    </custom:WaaaghWindow.TitleBarContent>-->
+
+    <!--<custom:WaaaghWindow.TitleBarButtons>
+        <Button Content="A"/>
+    </custom:WaaaghWindow.TitleBarButtons>-->
+
+
+    <Grid ShowGridLines="True">
+        <Grid.RowDefinitions>
+            <RowDefinition Height="32"/>
+            <RowDefinition Height="*"/>
+            <RowDefinition Height="32"/>
+        </Grid.RowDefinitions>
+
+        <Border Grid.Row="1">
+            <TextBlock FontSize="32" FontWeight="Bold" HorizontalAlignment="Center" VerticalAlignment="Center" Text="{Binding Greeting}"/>
+        </Border>
+    </Grid>
+
+</custom:WaaaghWindow>

+ 9 - 0
Waaagh/Views/MainWindow.xaml.cs

@@ -0,0 +1,9 @@
+using Waaagh.CustomControl;
+
+namespace Waaagh.Views {
+    public partial class MainWindow: WaaaghWindow {
+        public MainWindow() {
+            InitializeComponent();
+        }
+    }
+}

+ 49 - 0
Waaagh/Waaagh.csproj

@@ -0,0 +1,49 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+	<PropertyGroup>
+		<OutputType>WinExe</OutputType>
+		<TargetFramework>net8.0-windows</TargetFramework>
+		<Nullable>enable</Nullable>
+		<ImplicitUsings>enable</ImplicitUsings>
+		<UseWPF>true</UseWPF>
+	</PropertyGroup>
+
+	<ItemGroup>
+	  <None Remove="appsettings.json" />
+	</ItemGroup>
+
+	<ItemGroup>
+	  <Content Include="appsettings.json">
+	    <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+	  </Content>
+	</ItemGroup>
+
+	<ItemGroup>
+		<PackageReference Include="CommunityToolkit.Mvvm" Version="8.3.2" />
+		<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
+		<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0">
+		  <PrivateAssets>all</PrivateAssets>
+		  <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+		</PackageReference>
+		<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
+		<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.135" />
+	</ItemGroup>
+
+	<ItemGroup>
+		<Folder Include="Behaviors\" />
+		<Folder Include="CustomControl\Units\" />
+		<Folder Include="Themes\CustomControl\Units\" />
+		<Folder Include="Views\Units\" />
+	</ItemGroup>
+
+	<ItemGroup>
+		<ProjectReference Include="..\FakeLibrary\FakeLibrary.csproj" />
+	</ItemGroup>
+
+	<ItemGroup>
+	  <None Update="App.sqlite">
+	    <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+	  </None>
+	</ItemGroup>
+
+</Project>

+ 6 - 0
Waaagh/appsettings.json

@@ -0,0 +1,6 @@
+{
+  "ConnectionStrings": {
+    "Default": "Data Source=App.sqlite;"
+  },
+  "Test": "test"
+}

+ 9 - 0
WpfSample/App.xaml

@@ -0,0 +1,9 @@
+<Application x:Class="WpfSample.App"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:local="clr-namespace:WpfSample"
+             StartupUri="MainWindow.xaml">
+    <Application.Resources>
+         
+    </Application.Resources>
+</Application>

+ 7 - 0
WpfSample/App.xaml.cs

@@ -0,0 +1,7 @@
+using System.Windows;
+
+namespace WpfSample {
+    public partial class App: Application {
+    }
+
+}

+ 10 - 0
WpfSample/AssemblyInfo.cs

@@ -0,0 +1,10 @@
+using System.Windows;
+
+[assembly: ThemeInfo(
+    ResourceDictionaryLocation.None,            //where theme specific resource dictionaries are located
+                                                //(used if a resource is not found in the page,
+                                                // or application resource dictionaries)
+    ResourceDictionaryLocation.SourceAssembly   //where the generic resource dictionary is located
+                                                //(used if a resource is not found in the page,
+                                                // app, or any theme specific resource dictionaries)
+)]

+ 25 - 0
WpfSample/AttachedProperty.cs

@@ -0,0 +1,25 @@
+using System.Windows;
+using System.Windows.Controls;
+
+namespace WpfSample {
+    static class PropADemo {
+        public static string GetText(DependencyObject obj) {
+            return (string)obj.GetValue(TextProperty);
+        }
+
+        public static void SetText(DependencyObject obj, string value) {
+            obj.SetValue(TextProperty, value);
+        }
+
+        public static readonly DependencyProperty TextProperty =
+            DependencyProperty.RegisterAttached("Text", typeof(string), typeof(PropADemo),
+                new PropertyMetadata(string.Empty, OnTextChanged));
+
+        private static void OnTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
+            if (d is Control control) {
+
+            }
+        }
+
+    }
+}

+ 54 - 0
WpfSample/MainWindow.xaml

@@ -0,0 +1,54 @@
+<Window x:Class="WpfSample.MainWindow"
+        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:WpfSample"
+        xmlns:vm="clr-namespace:WpfSample.ViewModels"
+        mc:Ignorable="d"
+        Title="MainWindow" Height="450" Width="800"
+        BorderThickness="0" Background="Coral">
+
+    <Grid>
+
+        <Grid.RowDefinitions>
+            <RowDefinition Height="36"/>
+            <RowDefinition Height="*"/>
+        </Grid.RowDefinitions>
+
+        <StackPanel Grid.Row="0" Orientation="Horizontal">
+            <Button Command="{Binding AddItemCommand}" Content="Add"/>
+            <TextBlock x:Name="tb1" local:PropADemo.Text="Hello World"/>
+            <TextBlock x:Name="tb2" local:PropADemo.Text="Greeting"/>
+        </StackPanel>
+
+        <TreeView Grid.Row="1" ItemsSource="{Binding Groups}">
+            <TreeView.Resources>
+                <HierarchicalDataTemplate DataType="{x:Type vm:DemoGroupViewModel}" ItemsSource="{Binding Items}">
+                    <Border Background="Coral">
+                        <TextBlock Text="{Binding Message}"/>
+                    </Border>
+                </HierarchicalDataTemplate>
+
+                <HierarchicalDataTemplate DataType="{x:Type vm:DemoItemViewModel}">
+                    <Border Background="#669999">
+                        <TextBlock Text="{Binding Message}"/>
+                    </Border>
+                </HierarchicalDataTemplate>
+            </TreeView.Resources>
+
+            <TreeView.ItemContainerStyle>
+                <Style TargetType="TreeViewItem">
+                    <Setter Property="AllowDrop" Value="True"/>
+                    <EventSetter Event="MouseMove" Handler="TreeViewItem_MouseMove"/>
+                    <EventSetter Event="DragEnter" Handler="TreeViewItem_DragEnter"/>
+                    <EventSetter Event="DragOver" Handler="TreeViewItem_DragOver"/>
+                    <EventSetter Event="DragLeave" Handler="TreeViewItem_DragLeave"/>
+                    <EventSetter Event="Drop" Handler="TreeViewItem_Drop"/>
+                </Style>
+            </TreeView.ItemContainerStyle>
+
+        </TreeView>
+
+    </Grid>
+</Window>

+ 34 - 0
WpfSample/MainWindow.xaml.cs

@@ -0,0 +1,34 @@
+using System.Windows;
+using System.Windows.Input;
+using WpfSample.ViewModels;
+
+namespace WpfSample {
+    public partial class MainWindow: Window {
+        public MainWindow() {
+            InitializeComponent();
+            DataContext = new MainViewModel();
+        }
+
+        private void TreeViewItem_MouseMove(object sender, MouseEventArgs e) {
+            if (sender is FrameworkElement element && e.LeftButton == MouseButtonState.Pressed) {
+                DragDrop.DoDragDrop(element, element.DataContext, DragDropEffects.Move);
+            }
+        }
+
+        private void TreeViewItem_DragEnter(object sender, DragEventArgs e) {
+
+        }
+
+        private void TreeViewItem_DragOver(object sender, DragEventArgs e) {
+
+        }
+
+        private void TreeViewItem_DragLeave(object sender, DragEventArgs e) {
+
+        }
+
+        private void TreeViewItem_Drop(object sender, DragEventArgs e) {
+
+        }
+    }
+}

+ 34 - 0
WpfSample/ViewModels/DemoGroupViewModel.cs

@@ -0,0 +1,34 @@
+using System.Collections.ObjectModel;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+
+namespace WpfSample.ViewModels {
+    internal partial class DemoGroupViewModel: ObservableObject {
+        public DemoGroupViewModel() {
+            Random random = new Random();
+            for (int i = random.Next(1, 10); i >= 0; --i) {
+                Items.Add(new DemoItemViewModel() {
+                    Name = $"I{i}",
+                    Value = $"V{random.Next()}",
+                });
+            }
+        }
+
+        [ObservableProperty]
+        [NotifyPropertyChangedFor(nameof(Message))]
+        private string name = string.Empty;
+
+        public ObservableCollection<DemoItemViewModel> Items { get; } = new ObservableCollection<DemoItemViewModel>();
+
+        public string Message => $"{Name}-{Items.Count}";
+
+        [RelayCommand]
+        private void AddItem() {
+            Random random = new Random();
+            Items.Add(new DemoItemViewModel() {
+                Name = $"I{random.Next()}",
+                Value = $"V{random.Next()}",
+            });
+        }
+    }
+}

+ 15 - 0
WpfSample/ViewModels/DemoItemViewModel.cs

@@ -0,0 +1,15 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace WpfSample.ViewModels {
+    internal partial class DemoItemViewModel: ObservableObject {
+        [ObservableProperty]
+        [NotifyPropertyChangedFor(nameof(Message))]
+        private string name = string.Empty;
+
+        [ObservableProperty]
+        [NotifyPropertyChangedFor(nameof(Message))]
+        private string value = string.Empty;
+
+        public string Message => $"{Name}-{Value}";
+    }
+}

+ 30 - 0
WpfSample/ViewModels/MainViewModel.cs

@@ -0,0 +1,30 @@
+using System.Collections.ObjectModel;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+
+namespace WpfSample.ViewModels {
+    internal partial class MainViewModel: ObservableObject {
+        public MainViewModel() {
+            Random random = new Random();
+            for (int i = random.Next(1, 10); i >= 0; --i) {
+                Groups.Add(new DemoGroupViewModel() {
+                    Name = $"G{i}",
+                });
+            }
+        }
+
+        [ObservableProperty]
+        private string message = string.Empty;
+
+        public ObservableCollection<DemoGroupViewModel> Groups { get; set; } = new ObservableCollection<DemoGroupViewModel>();
+
+        class Test {
+            public string Name { get; set; } = string.Empty;
+        }
+
+        [RelayCommand]
+        private void AddItem() {
+            Groups[0].AddItemCommand?.Execute(null);
+        }
+    }
+}

+ 15 - 0
WpfSample/WpfSample.csproj

@@ -0,0 +1,15 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>WinExe</OutputType>
+    <TargetFramework>net8.0-windows</TargetFramework>
+    <Nullable>enable</Nullable>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <UseWPF>true</UseWPF>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="CommunityToolkit.Mvvm" Version="8.3.2" />
+  </ItemGroup>
+
+</Project>