基于 Prism 的可插拔标准 MES 架构设计

一、前言

在实际标准设备项目中,MES 对接通常不是单一客户,单一接口,单一协议的形式。真正有挑战的部分,往往来自以下几个方面:

1. 不同客户的协议完全不同,既有 TCP/IP,也有 HTTP,还有本地文件落地方案。
2. 不同客户支持的能力并不一致,有的支持登录校验,有的只支持结果上传。
3. MES 配置、业务流程、设备动作如果写在一起,会导致系统耦合度很高。
4. 新客户接入时,如果没有统一架构,往往需要从头复制和改造,开发效率很低。
5. 新客户接入项目一旦增多,后期维护成本会迅速上升。

当前这套解决方案的目标非常明确:将 MES 能力从业务项目中独立出来,沉淀成一套标准化、模块化、可扩展、可快速嵌入的架构,让其既能服务当前项目,也能在未来新的客户接入中快速复用。

整个解决方案由两个核心项目组成:

1. MesUse:宿主项目,负责承载和演示。
2. StandardMes:MES 标准模块,负责协议、配置、页面、调用封装与扩展机制。

Github地址:https://github.com/2825077535/MesUse.git


二、整体架构定位

从职责划分上看,这套demo做了比较清晰的分层。

1. 宿主层:MesUse

宿主项目主要负责:应用启动,主窗口承载,示例按钮调用,接收并处理 MES 发出的设备控制请求
宿主层不直接负责 MES 协议细节,而是负责与具体业务或设备控制代码打通。

2. 标准能力层:StandardMes
标准模块主要负责: 定义 MES 统一接口,封装多种 MES 客户端实现,维护配置模型,提供标准配置界面,提供统一请求与响应对象,提供安全调用封装,提供与宿主之间的事件通信机制
也就是说,StandardMes 本质上是一个“可嵌入的 MES 功能包”,宿主项目只需要完成最少的集成工作,就能把整套能力接入进去。


三、Prism 在架构中的作用

在常见的WPF项目中,prism架构是最常用的,由于我现在也是在使用prism架构,所有本次项目默认按prism项目来适配的。如果不是prism架构,可以将MesApp作为单例入口即可。

1. 模块化加载

protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
    moduleCatalog.AddModule<MesPluginModule>();
}


MesPluginModule 已模块的形式装载在项目中。这种方式的优势非常明显:宿主系统不需要直接管理大量 MES 细节,只需要装载模块即可。

2. 依赖注入
通过 Prism 的容器,MesApp、各类 View、ViewModel、Mes 客户端实现类都能够被自动解析。在 StandardMes 中,MesPluginModule 完成了核心类型注册:

public class MesPluginModule : IModule
public void RegisterTypes(IContainerRegistry containerRegistry)


3. 区域导航

<ContentControl prism:RegionManager.RegionName="MesViewRegion"/>整体配置页面的区域.
<ContentControl Grid.Row="0" prism:RegionManager.RegionName="{x:Static mesconst:MesConst.MesOptionsRegion}"/>作为客户UI的承载入口将配置页面与客户UI分离.
<ContentControl prism:RegionManager.RegionName="MesCustomerContentRegion"/>作为客户专属配置页面的区域

所以区域的挂载链路是:MesViewRegion作为整体配置页面的区域->MesOptionsRegion作为客户UI的承载入口将配置页面与客户UI分离->MesCustomerContentRegion作为客户专属配置页面的区域。


四、MES 架构的核心设计思想

通过 MesApp 统一入口,通过接口分层抽象能力,通过不同客户端实现隔离协议差异,通过 Prism 负责模块装配和页面组合。

1. MesApp:统一入口与工厂中心
MesApp 是整个 StandardMes 的核心门面对象,它负责:读取和保存配置, 持有当前激活的 MesClient,根据客户类型创建具体实现, 为上层提供统一调用入口

它内部维护了一个工厂字典:

private readonly Dictionary<MesEnum.MesCustomerEnum, Func<IMesClient>> _mesFactories = new();
RegisterMesFactory(MesEnum.MesCustomerEnum.LocalMes, () => _container.Resolve<LocalMes>());


当宿主调用 CreateMes() 时,会根据当前配置选择具体实现:

public async Task<bool> CreateMes()
{
    var customer = MesEnum.GetEnumValueFromDescription<MesEnum.MesCustomerEnum>(StandardMesConfig.SelectCustomer);

    if (_mesFactories.TryGetValue(customer, out var factory))
    {
        MesClient = factory();
    }
    else
    {
        MesClient = new DefaultMes();
    }

    return true;
}


将客户差异收敛在实现层,把创建逻辑集中在 MesApp 中管理。宿主只负责调用 MesApp,而不关心底层到底是 TCP、HTTP 还是本地文件。
2. 接口按能力拆分
在以前项目中,MES 接口会被定义成一个非常庞大的 IMesService,最终导致某些客户实现大量空方法或不支持方法。

当前架构把能力进行了拆分:IMesClient:基础客户端标识,IMesLoginService:登录能力,IMesResultService:结果上报与关闭能力,IMesBoardService:过板、取板、出板能力,IMesProgramService:程序相关能力,IMesDynamicService:动态扩展能力,IMesMachineStateService:设备状态能力,IMesAlarmService:报警能力
例如:

public interface IMesResultService
{
    Task<MesResponse> ResultAsync(MesResultRequest request);
    Task<MesResponse> CloseMesAsync(MesCloseRequest request);
}

这样设计的好处是:客户实现只需要实现自己真正支持的能力。不同协议的扩展边界清晰。上层代码更容易理解系统能力划分。新功能扩展时不容易牵动所有实现类。

这种做法非常适合实际中客户差异化明显的项目。

3. 请求与响应标准化

在 Contracts 目录中,架构为所有 MES 调用定义了统一的请求和响应模型。

请求模型包括:

- MesLoginRequest
- MesResultRequest
- MesBoardRequest
- MesRemovePcbRequest
- MesMachineStateRequest
- MesAlarmRequest
- MesProgramRequest
- MesDynamicRequest

响应模型包括:

- MesResponse
- MesResponse<TPayload>
- MesLoginResponse

标准响应对象定义如下:

public class MesResponse
{
    public bool Success { get; set; }
    public bool Enabled { get; set; } = true;
    public string Code { get; set; } = string.Empty;
    public string Message { get; set; } = string.Empty;
    public bool IsTimeout { get; set; }
    public string RawResponse { get; set; } = string.Empty;
}

统一模型最大的意义在于:

- 宿主代码不需要区分不同协议的返回格式。
- 上层业务可以统一处理成功、失败、超时、禁用和原始报文。
- 日志、重试、异常分析都具备统一入口。


五、安全调用封装:让业务层不关心底层细节

在这套架构中,宿主层不会直接把 MesClient 强转成某个接口再调用,而是通过扩展方法进行“安全调用”。

例如在 MainWindowViewModel 中,发送结果的代码是:

await _mesApp.ResultSafeAsync(request);

登录则是:

await _mesApp.MesLoginSafeAsync(new MesLoginRequest
{
    LoginName = "TestName",
    LoginPassword = "TestPassWord"
});

这些扩展方法统一定义在 MesSafeCallExtensions 中。它们负责做几件事:

1. 判断当前 MesClient 是否实现了目标接口。
2. 如果没有实现,返回统一的不可用响应。
3. 如果实现了,则发起真实调用。
4. 在调用过程中统一捕获异常、超时、取消和网络错误。
5. 把异常信息转换成标准 MesResponse。

核心逻辑如下:

private static async Task<TResult> SafeCallAsync<TService, TResult>(
    TService service,
    Func<TService, Task<TResult>> call,
    Func<TResult> fallback)
    where TService : class
    where TResult : MesResponse
{
    if (service == null)
    {
        return fallback();
    }

    try
    {
        return await call(service).ConfigureAwait(false);
    }
    catch (TimeoutException ex)
    {
        TResult response = fallback();
        FillExceptionResponse(response, ServiceTimeoutCode, "MES调用超时。", ex, true);
        return response;
    }
}


六、多客户实现如何落地

当前架构内置了三种典型的 MES 客户端实现:

1. LocalMes
2. TCPIPMes
3. HttpPostMes

这三者分别代表了本地文件型、长连接型和 Web 接口型 MES 对接方式。

1. LocalMes:本地文件输出模式

LocalMes 只实现 IMesResultService,将结果数据写入本地文件夹。
2. TCPIPMes:Socket 长连接模式
TCPIPMes 负责通过 TCP/IP 与外部 MES 通信。
使用 ConcurrentDictionary<Guid, TaskCompletionSource<TCPIPMesAcceptData>> 来维护等待响应的请求任务。由于TCPIP是原生的通讯协议,在异步通讯时经常会出现请求和响应不匹配的情况,所以通过一个全局的字典来维护每个请求对应的 TaskCompletionSource,当收到响应时,根据请求ID找到对应的 TaskCompletionSource 并设置结果,这样就能保证请求和响应的正确匹配。
3. HttpPostMes:HTTP 接口模式

HttpPostMes 则适用于标准 Web MES 接口场景。它的流程相对直观:

1. 构造 JSON 报文。
2. 使用 HTTP Post 发送请求。
3. 解析服务器返回数据。
4. 转换成标准 MesResponse。

例如登录逻辑:

public async Task<MesLoginResponse> MesLoginAsync(MesLoginRequest request)
{
    JObject pairs = new JObject
    {
        [nameof(Config.EquipmentID)] = Config.EquipmentID,
        [nameof(Config.LineID)] = Config.LineID,
        [nameof(request.LoginName)] = request.LoginName,
        [nameof(request.LoginPassword)] = request.LoginPassword
    };

    MesResponse response = await SendAsync("MesLogin", pairs.ToString()).ConfigureAwait(false);
    return new MesLoginResponse
    {
        Success = response.Success,
        Enabled = response.Enabled,
        Code = response.Code,
        Message = response.Message,
        RawResponse = response.RawResponse,
        IsTimeout = response.IsTimeout,
        CanEnter = response.Success,
        ErrorMessage = response.Message
    };
}


上层无论面对 TCP 还是 HTTP,实现的调用方式始终是统一的,而协议差异完全被隔离在内部实现中。


七、客户UI配置界面的模块化设计

这套架构不仅把通信层做了标准化,连配置界面也做了模块化。

1. MES 配置总入口

在 MesUse 的 ViewMes.xaml 中,定义了一个区域:

<ContentControl Grid.Row="0" prism:RegionManager.RegionName="MesOptionsRegion"/>

在 MesPluginModule 初始化时,将 MesAppView 注册到这个区域:

regionManager.RegisterViewWithRegion(MesConst.MesOptionsRegion, typeof(MesAppView));

也就是说,宿主只负责为 MES 配置区域留出位置,具体加载什么页面由模块自己决定。

2. 客户专属配置区域
MesAppView.xaml 中继续定义了一个内层区域:

<ContentControl prism:RegionManager.RegionName="MesCustomerContentRegion"/>

于是,整个配置界面形成了两层嵌套结构:

- 第一层:MES 总入口区域
- 第二层:客户专属配置区域

这种嵌套式 Region 设计非常适合复杂配置系统,因为不同客户通常拥有完全不同的配置项,但又需要统一挂在MesCustomerContentRegion入口下。项目只需要嵌入MesOptionsRegion的调度即可

3. 客户页面动态切换

在 MesStatic 中,维护了客户枚举到页面名称的映射:

internal static readonly Dictionary<MesEnum.MesCustomerEnum, string> _customerViewsMap =
    new Dictionary<MesEnum.MesCustomerEnum, string>()
{
    { MesEnum.MesCustomerEnum.LocalMes , nameof(Views.LocalMesView) },
    { MesEnum.MesCustomerEnum.TCPIPMes , nameof(Views.OutPutMesView) },
    { MesEnum.MesCustomerEnum.HttpPostMes , nameof(Views.OutPutMesView) }
};

当用户切换客户后,系统会根据映射进行导航:

regionManager.RequestNavigate(MesConst.MesCustomerContentRegion, targetView, navigationParameters);


八、配置与 ViewModel 解耦

在配置页面这部分,架构并没有直接让页面对配置实体进行硬编码读写,而是引入了 ViewModel 层并配合 AutoMapper 做对象映射。主要是用于确认配置与取消配置的操作。

在 ConfigProfile 中定义了映射关系:

CreateMap<LocalMesViewModel, CustomerConfig.LocalMesJson>().ReverseMap();
CreateMap<OutPutMesViewModel, CustomerConfig.OutPutMesJson>().ReverseMap();

对于新客户页面,也可以直接复用这种模式:
1. 新增客户配置实体。
2. 新增客户配置页面与 ViewModel。
3. 增加 AutoMapper 映射。
4. 增加页面导航映射。


九、宿主与标准模块之间如何解耦协作


除了项目调用Mes模块之外,这套架构还允许Mes调用项目内容:MES 模块可以向宿主发起设备动作请求,而不是只做单向接口调用。在实际项目中,例如Mes状态需要控制信号灯的亮灭,Mes报警需要控制报警器的响起,客户一键转产需求等,这些都直接通过Mes交互来控制设备。
这一点主要依赖 Prism 的 EventAggregator 实现。

1. 标准模块发布事件
在 MesModel 中,封装了设备动作请求方法,例如切换程序、打印日志、设备启动、设备停止、设备报警等。其底层统一通过 MesMachineEvent 发布消息:

EventAggregator.GetEvent<MesMachineEvent>().Publish(
    new MesControllerMachine(Name, Value, callback: (returnValue) =>
    {
        tcs.TrySetResult(returnValue);
    }, TimeSpan.FromMilliseconds(MesStatic.SetMesMachineControllerTimeOut), timeoutCts.Token));

它不仅发布动作名称和参数,还带有回调、超时和取消令牌,这是一次跨模块的异步命令调用。

2. 宿主侧订阅事件

在 MainWindowViewModel 中,宿主通过 EventAggregator 订阅该事件:

_mesMachineEventToken = _eventAggregator.GetEvent<MesMachineEvent>()
    .Subscribe(MesControllerMachine, ThreadOption.BackgroundThread);

宿主在回调中根据命令名称执行不同逻辑,例如:

switch (methodName)
{
    case MesConst.MachineLog:
        Console.WriteLine($"执行MachineLog:{valueStr}");
        isSuccess = true;
        break;
}

e.Callback?.Invoke(new MesControllerMachineReturn(isSuccess, errorMsg));

这代表着标准模块并不依赖任何具体设备控制代码,真正的设备动作始终由宿主项目决定。


十、整体调用流程说明

为了更直观地说明架构,下面按照实际代码路径梳理整个调用流程。

流程一:应用启动与模块装载

1. App 启动。
2. Prism 创建 MainWindow 作为 Shell。
3. 容器注册 MesApp 和导航页面。
4. 模块目录加载 MesPluginModule。
5. MesPluginModule 注册 MES 页面、ViewModel、客户端实现和映射组件。
6. MainWindow 加载后默认进入 View1。

这一流程完成的是:宿主应用初始化与标准模块装载。

流程二:进入 MES 配置界面

1. 用户点击 View1 中的“进入Mes配置”。
2. 系统导航到 ViewMes。
3. ViewMes 中的 MesOptionsRegion 加载 MesAppView。
4. MesAppView 再通过内部区域承载客户配置页面。
5. 根据当前客户配置,动态切换到 LocalMesView 或 OutPutMesView。

这一流程完成的是:MES UI 的嵌入与组合式展示。

流程三:创建当前客户的 MES 实例

当宿主点击“打开Mes”按钮时,会执行:

await _mesApp.CreateMes();

其内部会:

1. 读取当前配置中的客户类型。
2. 解析为 MesCustomerEnum。
3. 如果当前已有旧实例,则尝试关闭旧实例。
4. 从工厂字典中获取新客户对应的构造方法。
5. 创建新的 MesClient 并赋值给 MesApp。

这一流程完成的是:从配置驱动到具体实现实例化。

流程四:业务调用 MES 接口

以发送结果为例:

await _mesApp.ResultSafeAsync(request);

其内部调用链为:

1. 宿主 ViewModel 构造请求对象。
2. 通过 MesSafeCallExtensions 进入统一安全调用层。
3. 判断当前 MesClient 是否实现 IMesResultService。
4. 调用具体客户实现,例如 HttpPostMes.ResultAsync 或 TCPIPMes.ResultAsync。
5. 由 MesModel 统一构造结果报文。
6. 发送给真实 MES。
7. 返回标准 MesResponse。

这一流程完成的是:统一入口调用、协议适配与响应标准化。

流程五:MES 反向控制设备

当 StandardMes 需要通知宿主执行某些设备动作时,例如:

- 打印日志
- 报警
- 设备运行
- 设备停止
- 切换程序

则会通过 MesMachineEvent 发布命令,并等待宿主返回结果。宿主处理完成后,再通过回调把执行结果传回 StandardMes。

这一流程完成的是:MES 模块与设备控制系统之间的异步解耦协作。


十一、为什么这套架构适合快速嵌入项目

这篇文章的最重要的目标,是为什么这套架构适合作为一个标准能力快速嵌入到新的项目中。
主要原因有以下几点。

1. 宿主接入成本低

宿主只需要做少量工作:

- 引入 StandardMes 项目或程序集
- 在 App 中注册 MesApp
- 加载 MesPluginModule
- 提供一个区域承载配置页
- 订阅需要的设备事件

其余的配置页面、请求模型、调用封装、协议实现都已经在标准模块中准备好了。

2. 配置系统可直接复用

新项目不需要重新开发一套 MES 配置页面,因为当前架构已经把:

- 总入口页
- 客户选择页
- 客户专属配置页

完整拆分并可直接嵌入。

3. 业务调用方式统一

业务层面对不同客户时,调用方式完全一致,例如:

- MesLoginSafeAsync
- ResultSafeAsync
- CheckBoardSafeAsync
- OutBoardSafeAsync
- MachineStateSafeAsync
- AlarmInformationSafeAsync

这极大降低了业务开发复杂度。

4. 新客户接入边界清晰

如果未来接入新客户,通常只需要按固定步骤扩展:

1. 在 MesEnum 中新增客户枚举。
2. 新增一个客户实现类,如 XxxMes。
3. 实现需要支持的接口能力。
4. 在 MesPluginModule 中注册该实现。
5. 在 MesApp 中注册对应工厂。
6. 如果需要专属配置页面,则新增 View 和 ViewModel。
7. 在 MesStatic 中增加页面映射。

整个改动边界清晰、职责明确,不需要大面积修改宿主代码。



十二、总结

当前这套基于 Prism 的 MES 架构,本质上解决了两个核心问题:
1. 如何把 MES 做成可插拔、可嵌入、可复用的标准能力。
2. 如何在面对多客户差异化需求时,仍然保持清晰结构和快速交付能力。
在我自己实际的标准设备的项目接入Mes中。所遇到的客户的协议和要求可谓说是千奇百怪。这个整套架构也是我在数十个客户的接入经验基础上总结出来的。
主要还是针对嵌入现有项目,和新客户接入最快的完成内容的编码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值