Bread.Mvc 1.3.5

There is a newer version of this package available.
See the version list below for details.
dotnet add package Bread.Mvc --version 1.3.5
NuGet\Install-Package Bread.Mvc -Version 1.3.5
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Bread.Mvc" Version="1.3.5" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Bread.Mvc --version 1.3.5
#r "nuget: Bread.Mvc, 1.3.5"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Bread.Mvc as a Cake Addin
#addin nuget:?package=Bread.Mvc&version=1.3.5

// Install Bread.Mvc as a Cake Tool
#tool nuget:?package=Bread.Mvc&version=1.3.5

随着.net 7的发布,AOT编译日渐成熟,然而支持AOT的MVC框架却少之又少。在Avalonia发布11.0版本之后,这方面的需求将会进一步增加。Bread.Mvc 框架的目标就是探索Aot编译条件下实现一套灵活MVC框架的方法。

1. Ioc 容器

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 容器,请参见如下代码:

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。请参见如下代码:

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

    public static Command Save { get; } = new(nameof(ProjectCommands), 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 类。

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 类;
  • 必须在 ZeroIoCContainer 中注册,且必须使用 AddSingleton 注册;
  • 构造函数中的参数 Model 类也必须在 ZeroIoCContainer 中注册才能自动注入;
  • 相关 Command 的事件处理函数必须写在构造函数中;
  • Command 可挂接在不同的 Controller 中,但是不保证 Event 的执行顺序;
  • SessionController 实现了 IDisposable 接口,但是无需我们显示调用 Dispose 方法。请在应用程序结束时调用 IoC.Dispose() 清理。

2.3 Model

Model 连结业务逻辑和用户界面。用户输入(鼠标、键盘、触屏动作等)通过 Command 触发 Controller 中的业务流程,在 Controller 中更新 Model 的属性值,这些修改操作又立即触发用户界面的刷新。
定义:

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 声明以防止不必要的外部修改:

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 函数原型如下:

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 代码中完成依赖注入和属性监听。
请记住,监听的目的是为了响应业务变化以更新用户界面。

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 包含一些非常有用的扩展。
UI线程注入
Bread.Mvc.Avalonia.MainThreadDispatcher 实现了 IUIDispatcher,Model 的 Watch 操作会自动检查当前线程是否是主线程, 这个操作依赖 IUIDispatcher 接口。 所以您需要在Avalonia应用中注册这个服务:

 builder.AddSingleton<IUIDispatcher, MainThreadDispatcher>();

Reactive 为了简化 Watch 操作,为常见的控件添加更易用的绑定方法。


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

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

    public EngineSettingsPanel()
    {
        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 日志

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.3 配置文件读写

内置 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);
    }
}

Product Compatible and additional computed target framework versions.
.NET net7.0 is compatible.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on Bread.Mvc:

Package Downloads
Bread.Mvc.WPF

A collection of helper classes for WPF application base on Bread.Mvc.

Bread.Mvc.Avalonia

A collection of helper classes for Avalonia application base on Bread.Mvc.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.4.1 122 2/20/2024
1.4.0 118 1/23/2024
1.3.9 197 11/20/2023
1.3.7 140 10/10/2023
1.3.6 143 8/31/2023
1.3.5 154 8/17/2023
1.3.4.1 161 8/12/2023
1.3.4 161 8/7/2023
1.3.3 148 7/16/2023
1.3.1 130 6/27/2023
1.3.0 121 6/3/2023
1.2.0 153 4/30/2023
1.1.0 176 4/17/2023
1.0.6 163 4/13/2023
1.0.5 168 4/11/2023
1.0.4 176 3/27/2023
1.0.2 191 3/24/2023
1.0.1 180 3/21/2023
1.0.0 201 3/20/2023