[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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...