[Bread.Mvc] 開源一款自用 MVC 框架,支持 Native AOT

来源:https://www.cnblogs.com/coon/archive/2023/08/31/17669609.html
-Advertisement-
Play Games

# Bread.Mvc [Bread.Mvc](https://gitee.com/rizo/bread-mvc) 是一款完全支持 Native AOT 的 MVC 框架,搭配同樣支持 AOT 的 Avalonia,讓你的開發事半功倍。項目開源在 Gitee,歡迎 [Star](https://gi ...


Bread.Mvc

Bread.Mvc 是一款完全支持 Native AOT 的 MVC 框架,搭配同樣支持 AOT 的 Avalonia,讓你的開發事半功倍。項目開源在 Gitee,歡迎 Star

NuGet Status

1. Ioc 容器

IoC容器是 MVC 框架的核心,為了支持AOT,Bread.Mvc 框架選擇使用 ZeroIoC 作為 IoC 容器。ZeroIoC 是一款摒棄了反射的 IoC 容器,具有極高的性能並且完全相容AOT。為了支持 .net 7, 我對 ZeroIoC 代碼做了零星修改,重新發佈在 Bread.ZeroIoC

1.1 服務註冊

由於不能使用反射,ZeroIoc 使用 SourceGenerator 技術在編譯期生成註入代碼,這個機制依賴 ZeroIoCContainer 來觸發。ZeroIoCContainer 是部分類,並聲明瞭 Bootstrap 方法,用戶的註入註冊代碼必須放在這個方法中才會被自動生成。您可以將服務註冊類放在項目的不同地方,或者放在不同的項目中。請參見以下代碼實現自己的註冊類:

using Bread.Mvc;
using ZeroIoC;

namespace XDoc.Avalonia;

public partial class SessionContainer : ZeroIoCContainer
{
    protected override void Bootstrap(IZeroIoCContainerBootstrapper builder)
    {
        builder.AddSingleton<IAlertBox, AlertPacker>();
        builder.AddSingleton<IMessageBox, MessagePacker>();
        builder.AddSingleton<IUIDispatcher, MainThreadDispatcher>();

        builder.AddSingleton<Session>();
        builder.AddSingleton<SessionController>();
    }
}

1.2 IoC 容器初始化

需要使用 IoC.Init 方法初始化 IoC 容器,一般推薦在程式啟動之前完成服務註冊和 IoC 容器的初始化操作。請參見如下代碼:

using Bread.Mvc;

IoC.Init(new XDocContainer(), new SessionContainer());

為了幫助理解,可以查看 IoC.Init 函數的源代碼,就是將分佈在不同地方的多個註冊類合併為一個,大致如下所示:

public static void Init(params ZeroIoCContainer[] containers)
{
    foreach (var container in containers) {
        Resolver.Merge(container);
    }

    Resolver.End();
}

2. MVC 架構

2.1 Command

聲明:

用戶的輸入被抽象為Command,Command 連接用戶界面和 Controller。請參見如下代碼聲明自己的 Command :

public static class AppCommands
{
    public static Command Load { get; } = new(nameof(AppCommands), nameof(Load));

    public static Command Save { get; } = new(nameof(AppCommands), nameof(Save));

    public static AsyncCommand<string, string> ImportAsync { get; } = new(nameof(AppCommands), nameof(ImportAsync));

    public static Command Delete { get; } = new(nameof(AppCommands), nameof(Delete));
}

有兩種類型的 Command, 普通 Command 和 AsyncCommand。如您所見, AsyncCommand 支持非同步操作。

使用:

一般我們我在 xaml 或 axaml 的尾碼代碼文件中使用 Command,表示響應用戶的輸入。

private void UiListBox_SelectionChanged(object? sender, SelectionChangedEventArgs e)
{
    if (e.AddedItems == null || e.AddedItems.Count == 0) return;
    if (e.AddedItems[0] is not ImageItemViewModel img) return;
    if (img == _session.CurrentImage) return;

    SessionCommands.SwitchImage.Execution(img);
}

private void UiBtnRight_Click(object? sender, global::Avalonia.Interactivity.RoutedEventArgs e)
{
    SessionCommands.NextImage.Execution();
}

private void UiBtnLeft_Click(object? sender, global::Avalonia.Interactivity.RoutedEventArgs e)
{
    SessionCommands.PreviousImage.Execution();
}

2.2 Controller

Controller 是業務邏輯的入口,您將在這裡集中處理程式的各種邏輯。在上面 IoC 註冊的例子中,SessionController 就是一個我們自己定義的 Controller 類。
Controller 子類能自動註入已註冊過的服務(Model)。請儘可能使用組合模式以防止 Controller 代碼體積膨脹。

public class SessionController : Controller, IDisposable
{
    readonly AppModel _app;
    readonly Session _session;
    readonly ProjectModel _prj;

    SerialTaskQueue<Doc?> _loadTask = new();

    public SessionController(AppModel app, Session session, ProjectModel prj)
    {
        _app = app;
        _prj = prj;
        _session = session;

        SessionCommands.SwitchData.Event += SwitchData_Event;
        SessionCommands.SwitchDoc.Event += SwitchDoc_Event;
        SessionCommands.SwitchImage.Event += SwitchImage_Event;

        SessionCommands.NextImage.Event += NextImage_Event;
        SessionCommands.PreviousImage.Event += PreviousImage_Event;

        SessionCommands.SaveDoc.Event += SaveDoc_Event;
        SessionCommands.NextDoc.Event += NextDoc_Event;

        _loadTask.Start();

        _prj.Loaded += _prj_Loaded;
    }
}

有以下幾點需要特別註意:

  • 必須繼承自 Controller 類才會被 Ioc 初始化時自動實例化(避免沒有顯式獲取時 Command 的 Event 事件不被掛接);
  • 所有Controller都是單例模式,必須使用 AddSingleton 註冊,防止 Command 事件掛接後被多次觸發;
  • 構造函數中的參數 Model 類也必須在 ZeroIoCContainer 中註冊才會自動註入;
  • 相關 Command 的事件處理函數必須寫在構造函數中;
  • Command 可掛接在不同的 Controller 中,但是不保證執行順序;
  • SessionController 實現了 IDisposable 介面,但是無需我們顯式調用 Dispose 方法。請在應用程式結束時調用 IoC.Dispose() 清理。

2.3 Model

Model 連結業務邏輯和用戶界面。用戶輸入(滑鼠、鍵盤、觸屏動作等)通過 Command 觸發 Controller 中的業務流程,
在 Controller 中更新 Model 的屬性值,這些修改操作又立即觸發用戶界面的刷新。
邏輯是閉環的:UI->Command->Controller->Model->UI。

定義:

源代碼中對 Model 的定義相當簡單,只是聲明必須要實現 INotifyPropertyChanged 介面。

public abstract class Model : INotifyPropertyChanged
{
    public bool IsDataChanged { get; set; }

    public event PropertyChangedEventHandler? PropertyChanged;

    protected virtual void OnPropertyChanged(string name)
    {
        IsDataChanged = true;
        this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }
}

聲明:

一般我們將 Model 和相關的 Controller 聲明在一個類庫中,並用 internal set 修飾以防止不必要的外部修改。建議您也只在對應的 Controller 中修改 Model 的屬性。不加限制的修改 Model 對象的屬性,只會帶來更多的屎山代碼。

public class ProjectModel : Model
{
    public int Volume { get; internal set; } = 3;

    public RangeList<Volume> Volumes { get; } = new();

    public string NewDocFolder { get; internal set; } = string.Empty;

    public RangeList<NewDoc> NewDocs { get; } = new();

    public ProjectModel()
    {
    }
}

推薦使用 PropertyChanged.Fody 自動實現 INotifyPropertyChanged 介面。
事實上因為實現了 INotifyPropertyChanged 介面, 您可以在xaml直接綁定 Model 中的屬性。

使用:

我們使用 Watch 函數監聽 Model 屬性的變化,Watch 和 UnWatch 函數的原型如下:

public static void Watch(this INotifyPropertyChanged publisher, string propertyName, Action callback);
public static void Watch(this INotifyPropertyChanged publisher, Action callback, params string[] propertyNames);
public static void UnWatch(this INotifyPropertyChanged publisher, string name, Action callback);
public static void UnWatch(this INotifyPropertyChanged publisher, Action callback, params string[] propertyNames);

通常我們在 Window 或者 UserControl 的 Load 代碼中完成依賴註入和屬性監聽。
你可以一次監聽一個屬性,或同時監聽多個屬性併在一個 Action 中響應這些屬性的變化。

請記住,監聽的目的是為了響應業務變化以同步更新用戶界面。

private void ImageSlider_Loaded(object? sender, global::Avalonia.Interactivity.RoutedEventArgs e)
{
    if (Design.IsDesignMode) return;

    _session = IoC.Get<Session>();  // 從 IoC 容器中取出實例, Session 必須先註冊。
    _session.Watch(nameof(Session.CurrentImage), Session_CurrentImage_Changed); // 監聽 CurrentImage 屬性的變化

    uiListBox.ItemsSource = _session.Images; // UI元素直接綁定 Model 中的屬性
    uiListBox.SelectionChanged += UiListBox_SelectionChanged;
}

3. 其他基礎設施

3.1 Avalonia

當您的應用平臺是 Avalonia 時,Bread.Mvc.Avalonia 包含一些非常有用的擴展。

IUIDispatcher 介面 :UI線程註入

Bread.Mvc.Avalonia.MainThreadDispatcher 實現了 IUIDispatcher 介面。
因為當屬性被外部線程修改時,Watch 機制需要使用這個介面檢測當前線程是否在主線程中,並將變更 Invoke 給UI線程,所以您必須在Avalonia應用中註冊這個服務。

 builder.AddSingleton<IUIDispatcher, Bread.Mvc.Avalonia.MainThreadDispatcher>();

Reactive

為了簡化 Watch 操作,我們為常見的控制項準備了更易用的綁定方法。


public interface IEnumDescriptioner<T> where T : Enum
{
    string GetDescription(T value);
}

public partial class SettingsPanel : UserControl
{
    SpotModel _spot = null!;

    public SettingsPanel()
    {
        InitializeComponent();

        if (Design.IsDesignMode) return;

        _spot = IoC.Get<SpotModel>();

        // combox initted by enum which LanguageHelper implements IEnumDescriptioner
        uiComboxLanguage.InitBy(new LanguageHelper(), Language.Chinese, 
            Language.English, Language.Japanese, Language.Japanese); 

        uiComboxLanguage.BindTo(_spot, m => m.Language); // ComboBox
       
        uiNUDAutoSave.BindTo(_app, x => x.AutoSave); // NumericUpDown
        uiTbRegCode.BindTo(_app, x => x.RegCode); // TextBox
        uiTbFilePath.BindTo(_app, x => x.FilePath); // TextBlock

        uiSlider.BindTo(_app, x => x.Progress); // Slider

        uiSwitchAutoSpot.BindTo(_spot, m => m.IsAutoSpot); // SwitchButton
        uiTbtnChannel.BindTo(_app, x => x.IsLeftChannel); // ToggleButton

        uiCheckSexual.BindTo(_app, x => x.IsMale); // CheckBox
    }
}

3.2 WPF

略,不想多說。

3.3 日誌

Bread.Utility 中提供了一個簡單的日誌類 Log。

public static class Log
{
    /// <summary>
    /// 打開日誌
    /// </summary>
    /// <param name="path">日誌文件名稱</param>
    /// <param name="expire">日誌文件目錄下最多保存天數。0表示不刪除多餘日誌</param>
    /// <exception cref="ArgumentNullException"></exception>
    public static void Open(string path, int expire = 0);

    /// <summary>
    /// 關閉日誌文件
    /// </summary>
    public static void Close();

    public static void Info(string info, string? category = null,
        [CallerFilePath] string? className = null,
        [CallerMemberName] string? methondName = null,
        [CallerLineNumber] int lineNumber = 0);

    public static void Warn(string warn, string? category = null,
        [CallerFilePath] string? className = null,
        [CallerMemberName] string? methondName = null,
        [CallerLineNumber] int lineNumber = 0);

    public static void Error(string error, string? category = null,
        [CallerFilePath] string? className = null,
        [CallerMemberName] string? methondName = null,
        [CallerLineNumber] int lineNumber = 0);

    public static void Exception(Exception ex);
}

3.4 配置文件讀寫

內置 Config 類用於 ini 文件讀寫。

public class CustomController : Controller
{
    Config _appConfig;
    readonly AppModel _app;
    readonly ProjectModel _prj;

    public AppController(AppModel app, ProjectModel prj)
    {
        _app = app;
        _prj = prj;
        
        _appConfig = new Config(Path.Combine(app.AppFolder, "app.data"));
    
        AppCommands.Load.Event += Load_Event;
        AppCommands.Save.Event += Save_Event;
    }

    private void Load_Event()
    {
        _appConfig.Load();
        _app.LoadFrom(_appConfig);
        _prj.LoadFrom(_appConfig);
    }

    private void Save_Event()
    {
        _app.SaveTo(_appConfig);
        _prj.SaveTo(_appConfig);
        _appConfig.Save();
    }
}
public class AppModel : Model
{
    public string Recorder { get; internal set; } = string.Empty;

    public ReadOnlyCollection<string> RecentList { get { return _recentList.AsReadOnly(); } }

    List<string> _recentList = new();

    public AppModel()
    {
    }

    public override void LoadFrom(Config config)
    {
        config.Load(nameof(AppModel), nameof(Recorder), (string value) => { Recorder = value; });

        var list = config.LoadList(nameof(RecentList));
        foreach (var item in list) {
            if (File.Exists(item)) {
                _recentList.Add(item);
            }
        }
        OnPropertyChanged(nameof(RecentList));
    }


    public override void SaveTo(Config config)
    {
        base.SaveTo(config);

        config[nameof(AppModel), nameof(Recorder)] = Recorder;
        config.SaveList(nameof(RecentList), _recentList);
    }
}

4. 限制

  • 只支持 .net 7 及之後的版本;
  • 不支持 asp.net core;

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ## 常用經驗 - 在HTTP中,我們要通過 URL 進行資源的定位 >比如: > >要取 id=888 的用戶信息,我們就向/user/{id} 這個路徑發送請求, > >要取 id=888 的用戶的訂單列表,我們就向/user/{id}/orders 這個路徑發送請求 - 在HTTP 中,DEL ...
  • Excel是一種常用的電子錶格軟體,廣泛應用於金融、商業和教育等領域。它提供了強大的數據處理和分析功能,可進行各種計算和公式運算,並能創建各種類型的圖表和可視化數據。Excel的靈活性使其成為處理和管理數據的重要工具。本文將介紹如何使用 Spire.XLS for Python 通過代碼創建Exce ...
  • ## 1、前言 作為一名後臺開發人員,許可權這個名詞應該算是特別熟悉的了。就算是java里的類也有 public、private 等“許可權”之分。之前項目里一直使用shiro作為許可權管理的框架。說實話,shiro的確挺強大的,但是它也有很多不好的地方。shiro預設的登錄地址還是login.jsp,前 ...
  • ### 一、簡介 Spring框架提供了一種名為Spring Cache的緩存策略。Spring Cache是一種抽象層,它提供了一種方便的方式來管理緩存,並與Spring應用程式中的各種緩存實現(如EhCache、Guava、Caffeine等)集成。 Spring Cache使用註解(如@Cac ...
  • 程式設計領域的`設計模式的六大設計原則` + `合成復用原則`(Composite Reuse Principle) ,都是一些很**泛**的思想(它們既可以指這個,也可以代指那個),無法生搬硬套,無法做到很具體的指導。我的建議是,有空多看幾遍、多思考看看怎麼能運用在實際項目中,在未來時**保佑** ...
  • ## 前言 一款app,消息頁面有:錢包通知、最近訪客等各種通知類別,每個類別可能有新的通知消息,實現已讀、未讀功能,包括多少個未讀,這個是怎麼實現的呢?比如用戶A訪問了用戶B的主頁,難道用rabitmq給B發通知消息嗎?量大了成本受得了嗎?有沒有成本低的方案呢 ![img](https://img ...
  • 來源:進擊雲原生 ### 1、檢測兩台伺服器指定目錄下的文件一致性 ``` #!/bin/bash ###################################### 檢測兩台伺服器指定目錄下的文件一致性 ##################################### #通過對 ...
  • 向ES發送請求時,如何創建請求對象呢?官方推薦的builder patter,在面對複雜的請求對象結構時還好用嗎?有沒有更加直觀簡潔的方法,盡在本文一網打盡 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...