首页 > 其他分享 >[Bread.Mvc] 开源一款自用 MVC 框架,支持 Native AOT

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

时间:2023-08-31 15:13:53浏览次数:46  
标签:string Mvc void Event MVC AOT static app public

Bread.Mvc

NuGet Status

Bread.Mvc 是一款完全支持 Native AOT 的 MVC 框架,搭配同样支持 AOT 的 Avalonia,让你的开发事半功倍。项目开源在 Gitee,欢迎 Star

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;

标签:string,Mvc,void,Event,MVC,AOT,static,app,public
From: https://www.cnblogs.com/coon/p/17669609.html

相关文章

  • <mvc:annotation-driven />, <context:annotation-config/>, <context:component-scan
    <mvc:annotation-driven/> 会做以下几件事: 向spring容器中注册DefaultAnnotationHandlerMapping。向spring容器中注册AnnotationMethodHandlerAdapter。配置一些messageconverter。解决了@Controller注解的使用前提配置,即HandlerMapping能够知道谁来处理请求。<cont......
  • <mvc:annotation-driven>和DefaultAnnotationHandlerMapping
    spring配置拦截器有两种方式: <mvc:annotation-driven/><!--*************openSessionInViewInterceptor*************--><beanid="openSessionInViewInterceptor"class="org.springframework.orm.hibernate3.support.OpenSessionInViewInt......
  • [回馈]ASP.NET Core MVC开发实战之商城系统(完:内附源码)
    经过一段时间的准备,【ASP.NETCoreMVC开发实战之商城系统】已经完成,目前代码已开发完成,先将全部内容整理分享,如有不足之处,还请指正。 商城系统主要功能 本系列文章主要讲解了商城系统的整体功能设计,页面布局设计,环境搭建,系统配置,及首页【商品类型,banner条,友情链接,降价促销......
  • day127-springMVC的介绍与入门
    springMVC介绍与初始化介绍MVC是一种软件架构的思想,将软件按照模型、视图、控制器来划分M:Model,模型层,指工程中的JavaBean,作用是处理数据JavaBean分为两类:一类称为实体类Bean:专门存储业务数据的,如Student、User等一类称为业务处理Bean:指Service或Dao对象,专门用于处理......
  • springMvc常用注解与作用
    @Controller注解的作用在SpringMVC中,控制器Controller负责处理由DispatcherServlet分发的请求,它把用户请求的数据经过业务处理层处理之后封装成一个Model,然后再把该Model返回给对应的View进行展示。在SpringMVC中提供了一个非常简便的定义Controller的方......
  • SpringMVC-cnblog
    SpringMVCspringmvc原理1.配置web.xml资源2.编写配置文件3.编写Controllerspringmvc底层原理HelloSpringMVC(配置版)新建一个Moudle,添加web的支持!确定导入了SpringMVC的依赖!配置web.xml,注册DispatcherServlet<?xmlversion="1.0"encoding="UTF-8"?>......
  • SpringMVC3的ResponseBody返回字符串乱码问题解决
    SpringMVC的@ResponseBody注解可以将请求方法返回的对象直接转换成JSON对象,但是当返回值是String的时候,中文会乱码 原因是因为其中字符串转换和对象转换用的是两个转换器,而String的转换器中固定了转换编码为"ISO-8859-1" 网上也很多种解决方法,有通过配置Bean编码的,也有自己重写转......
  • Springboot和SpringMVC
    SpringBoot是一个用于创建独立的、基于Spring框架的Java应用程序的开源框架。它简化了以往需要在Spring应用中进行繁琐配置的过程,提供了开箱即用的配置和约定,使得开发者能够更集中精力于业务逻辑的实现。SpringBoot提供了许多优秀的特性,其中包括:1.自动配置(Auto-config......
  • 开发模式:ASP.NET MVC4
    1)新建ASP.NETMVC4网站项目;2)引用前面的Data和Service工程项目;3)向网站项目添加install-packageautofac.mvc4install-packageautomapper-version4.1.14)新建ViewModels、Mappings两个文件夹;5)生成必要的ViewModels;6)向Mappings中复制文件:AutoMapperConfiguration.cs......
  • [回馈]ASP.NET Core MVC开发实战之商城系统(六)
    经过一段时间的准备,新的一期【ASP.NETCoreMVC开发实战之商城系统】已经开始,在之前的文章中,讲解了商城系统的整体功能设计,页面布局设计,环境搭建,系统配置,及首页【商品类型,banner条,友情链接,降价促销,新品爆款】,商品列表页面,商品详情,购物车等功能的开发,今天继续讲解订单管理功能开发,......