[Bread.Mvc] 开源一款自用 MVC 框架,支持 Native AOT
Bread.Mvc
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;
[Bread.Mvc] 开源一款自用 MVC 框架,支持 Native AOT的更多相关文章
- 开源:Taurus.MVC-Java 版本框架 (支持javax.servlet.*和jakarta.servlet.*双系列,内集成微服务客户端)
版本说明: 因为之前有了Taurus.MVC-DotNet 版本框架,因此框架标了-Java后缀. .Net 版本: 开源文章:开源:Taurus.MVC-DotNet 版本框架 (支持.NET C ...
- Asp.net Mvc模块化开发之分区扩展框架
对于一个企业级项目开发,模块化是非常重要的. 默认Mvc框架的AreaRegistration对模块化开发真的支持很好吗?真的有很多复杂系统在使用默认的分区开发的吗?我相信大部分asp.net的技术团 ...
- MVC(Model View Controller)框架
MVC框架 同义词 MVC一般指MVC框架 MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一 ...
- 几款开源的hybird移动app框架分析
几款开源的Hybrid移动app框架分析 Ionic Onsen UI 与 ionic 相比 jQuery Mobile Mobile Angular UI 结论 很多移动开发者喜欢使用原生代码开发, ...
- 推荐一款开源的C#TCP通讯框架
原来收费的TCP通讯框架开源了,这是一款国外的开源TCP通信框架,使用了一段时间,感觉不错,介绍给大家 框架名称是networkcomms 作者开发了5年多,目前已经停止开发,对于中小型的应用场景,够 ...
- MVC 插件化框架支持原生MVC的Area和路由特性
.NET MVC 插件化框架支持原生MVC的Area和路由特性 前面开放的源码只是简单的Plugin的实现,支持了插件的热插拔,最近晚上偶然想到,原生的MVC提供Areas和RouteAtrribut ...
- 基于存储过程的MVC开源分页控件
基于存储过程的MVC开源分页控件--LYB.NET.SPPager 摘要 现在基于ASP.NET MVC的分页控件我想大家都不陌生了,百度一下一大箩筐.其中有不少精品,陕北吴旗娃杨涛大哥做的分页控件M ...
- MVC+Spring.NET+NHibernate .NET SSH框架整合 C# 委托异步 和 async /await 两种实现的异步 如何消除点击按钮时周围出现的白线? Linq中 AsQueryable(), AsEnumerable()和ToList()的区别和用法
MVC+Spring.NET+NHibernate .NET SSH框架整合 在JAVA中,SSH框架可谓是无人不晓,就和.NET中的MVC框架一样普及.作为一个初学者,可以感受到.NET出了MV ...
- Spring MVC+Spring+Mybatis+MySQL(IDEA)入门框架搭建
目录 Spring MVC+Spring+Mybatis+MySQL(IDEA)入门框架搭建 0.项目准备 1.数据持久层Mybatis+MySQL 1.1 MySQL数据准备 1.2 Mybatis ...
- IDEA02 利用Maven创建Web项目、为Web应用添加Spring框架支持、bean的创建于获取、利用注解配置Bean、自动装配Bean、MVC配置
1 环境版本说明 Jdk : 1.8 Maven : 3.5 IDEA : 专业版 2017.2 2 环境准备 2.1 Maven安装及其配置 2.2 Tomcat安装及其配置 3 详细步骤 3.1 ...
随机推荐
- ubuntu配置vscode全过程(下载安装配置优化插件)
一.安装vscode 下载vscode 当然啦,我们安装vscode,当然要先下载啦,但是但是但是!不要在ubuntu的软件中心(Ubuntu Software)下载!贼坑!下载完不能用! 推荐下载方 ...
- rt下降40%?程序并行优化六步法
1 背景 性能优化是我们日常工作中很重要的一部分,主要有以下原因: 降低服务器和带宽等硬件成本:用更少的资源处理更多的请求 提高现实世界的运行效率:人机处理效率存在数量级的偏差,同样机器世界的效率提升 ...
- itextpdf5.5.13给pdf添加图片水印、添加文字水印(平铺)、添加文字水印(单个)、添加页眉、页脚、页眉事件、添加图片
转载自简书用户:alex很累,感谢分享.原地址:https://www.jianshu.com/p/2b9c7a0300e4 一.相关工具类 1. Excel2Pdf.java (如代码不可用请查看原 ...
- celery笔记一之celery介绍、启动和运行结果跟踪
本文首发于公众号:Hunter后端 原文链接:celery笔记一之celery介绍.启动和运行结果跟踪 本篇笔记内容如下: celery 介绍 celery 准备 celery 启动和异步任务的运行 ...
- 万字长文讲透 RocketMQ 4.X 消费逻辑
RocketMQ 是笔者非常喜欢的消息队列,4.9.X 版本是目前使用最广泛的版本,但它的消费逻辑相对较重,很多同学学习起来没有头绪. 这篇文章,笔者梳理了 RocketMQ 的消费逻辑,希望对大家有 ...
- CKS 考试题整理 (01)-NetworkPolicy
Task 创建一个名为 pod-restriction 的 NetworkPolicy 来限制对在 namespace dev-team 中运行的 Pod products-service 的访问. ...
- 3. HelloWorld的实现
恐惧是本能,行动是信仰(在此感谢尚硅谷宋红康老师的教程) 1. 新建 Project - Class 选择"New Project": 指名工程名.使用的 JDK 版本等信息.如下 ...
- Rainbond助力“信创应用”迁移上云
Rainbond v5.14.2 版本,又称信创版本.从这个版本开始,开源用户也可以利用 Rainbond 管理符合信创要求的硬件计算资源.在这个版本中,产品团队将此前只在企业版产品中存在的信创相关功 ...
- 如何将mp4文件解复用并且解码为单独的.yuv图像序列以及.pcm音频采样数据?
一.初始化解复用器 在音视频的解复用的过程中,有一个非常重要的结构体AVFormatContext,即输入文件的上下文句柄结构,代表当前打开的输入文件或流.我们可以将输入文件的路径以及AVFormat ...
- BeanDefinitionStoreException: Failed to read candidate component class
ssm 整合时出现问题 org.springframework.beans.factory.BeanDefinitionStoreException: Failed to read candidate ...