WPF中同类型实体间的消息推送
1. 引言
最近在重构一个C/S项目,按照构想,需要把原有的各个功能点抽出来做成插件。这样的话,在辅之以配置功能,可以在部署时动态配置界面功能。当积累的插件达到一定的数量时,通过插件重用,仅通过配置就可以实现需求的业务功能。但是,数据采集层和业务层之间的数据交互方式,是一个需要解决的问题。在旧项目中,数据采集层将采集到的数据放到一个公共的数据域中,业务层模块通过定时器定时从数据域中拉取数据,并显示到界面上。但在重构后,每一个功能点都作为要给功能插件存在,业务模块是通过组合不同的功能插件来实现的。因此,原有的数据获取模式虽然也可以在新项目中使用,但考虑到这样需要在各插件中均添加一个定时器,这样不但会消耗更多的资源,而且从开发上来看,也会增加很多重复的工作量。所以,我就考虑在采集层主动将数据推送到各功能插件上去。下面就说一下具体的实现过程。
2. 业务层和数据采集层的数据交互
下图是项目的实现思路,这里我仅展示了主程序-业务层-数据采集层的内容,毕竟有这些东西足够说明我们要说的事情。
从图上我们可以看到,管理插件加载各个业务功能插件,并将之组装成模块界面(这里需要根据具体的配置信息去装配,图上没有表现出配置信息这块内容),主程序获取管理插件组装好的模块界面,并显示出来;业务插件通过ComFactory获取采集层插件解析好的设备数据。注意一下ICom<T>,这里的T就是设备数据模型。我们要想办法在T的不同实例间进行数据推送,当T的一个实例的某一个属性值改变时,其他实例对应的熟悉也会接收到这个改变。
3. 实例间通信
解决实例间属性通知问题,我们用的是UDP。UDP的作用是监听来自实例的属性改变通知,收到通知后通过事件将通知分发到各个实例。具体实现如下:
using Newtonsoft.Json; using System; using System.Diagnostics; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Text; using System.Threading.Tasks; namespace NotifyPropertyChangedBetweenInstance.Common { public class UdpNotify : IDisposable { private UdpClient _server; private UdpClient _client; // 在本机进行UDP通信 private readonly string _ip = "127.0.0.1"; private bool _canContinue = true; private bool _isRunning = false; // 默认发送端口 private static int _sendPort = 9095; // 默认接收端口 private static int _receivePort = 9096; // 进程Id,用于在UdpNotifyPropertyChanged事件中判断消息是否来源于同一个进程 public static int ProcessId; // 通知事件,告知其他实例属性值已改变 public event Action<object, UdpNotifyEventArgs> UdpNotifyPropertyChanged; public UdpNotify() { ProcessId = Process.GetCurrentProcess().Id; _sendPort = GetIdleUdpPort(_sendPort); _receivePort = GetIdleUdpPort(_sendPort + 1 > _receivePort ? _sendPort + 1 : _receivePort); if (_sendPort < 0 || _receivePort < 0) throw new Exception(@"UDP端口资源已耗尽,未能找到空闲端口"); _server = new UdpClient(_sendPort); _client = new UdpClient(_receivePort); ReceiveMessage(); } public void Send(UdpNotifyEventArgs e) { var str = JsonConvert.SerializeObject(e); var bs = Encoding.UTF8.GetBytes(str); _server?.Send(bs, bs.Length, _ip, _receivePort); } private async void ReceiveMessage() { if (_isRunning) return; _isRunning = true; while (_canContinue) { await ReceiveAsync(); } } private async Task ReceiveAsync() { var task = Task.Factory.StartNew(() => { var point = new IPEndPoint(IPAddress.Parse(_ip), _receivePort); var bs = _client.Receive(ref point); return bs; }); await task; if (!task.IsCompleted) return; var str = Encoding.UTF8.GetString(task.Result); var e = JsonConvert.DeserializeObject<UdpNotifyEventArgs>(str); UdpNotifyPropertyChanged?.Invoke(this, e); } /// <summary> /// UDP端口是否被占用 /// </summary> /// <param name="port"></param> /// <returns></returns> private bool IsUdpPortUsed(int port) { var props = IPGlobalProperties.GetIPGlobalProperties(); var points = props.GetActiveUdpListeners(); foreach (var point in points) { if (point.Port == port) return true; } return false; } /// <summary> /// 获取空闲UDP端口 /// </summary> /// <param name="startPort"></param> /// <returns></returns> private int GetIdleUdpPort(int startPort) { var port = startPort; while (port < 65535) { if (IsUdpPortUsed(port)) { port++; continue; } return port; } return -1; } public void Dispose() { _canContinue = false; if (_server != null) { _server.Close(); _server.Dispose(); _server = null; } if (_client != null) { _client.Close(); _client.Dispose(); _client = null; } _isRunning = false; } } }
UdpNotifyEventArgs类是这样定义的:
using System; namespace NotifyPropertyChangedBetweenInstance.Common { public class UdpNotifyEventArgs : EventArgs { /// <summary> /// 改变后的属性值 /// </summary> public object Value { get; set; } /// <summary> /// 属性名称 /// </summary> public string PropertyName { get; set; } /// <summary> /// 与属性对应的私有变量名称 /// </summary> public string PrivatePropertyName { get; set; } /// <summary> /// 属性所属的类 /// </summary> public string ClassType { get; set; } /// <summary> /// 属性所属的实体的哈希值 /// </summary> public long HashCode { get; set; } /// <summary> /// 进程Id,本功能仅用于进程内通信, /// 接收方通过ProcessId判断信息是否来自于同一进程 /// </summary> public int ProcessId { get; set; } } }
通过UdpNotify,就可以实现各个设备数据实体间的通信了。
4. 设备实体基类NotifyPropertyChangeBase
using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Linq; using System.Reflection; namespace NotifyPropertyChangedBetweenInstance.Common { public class NotifyPropertyChangeBase : INotifyPropertyChanged, IDisposable { // 这里要声明为静态的 private static UdpNotify _udpNotify; public event PropertyChangedEventHandler PropertyChanged; /// <summary> /// 是否是发送者 /// </summary> public bool IsSender { get; set; } /// <summary> /// 是否从发送者接收数据, /// 用于处理不需要接收属性改变的情况 /// </summary> public bool IsAcceptDataFromSender { get; set; } /// <summary> /// 实例所在进程的进程Id /// </summary> public int ProcessId { get; } public NotifyPropertyChangeBase() { IsSender = false; IsAcceptDataFromSender = true; ProcessId = Process.GetCurrentProcess().Id; if (_udpNotify == null) _udpNotify = new UdpNotify(); _udpNotify.UdpNotifyPropertyChanged += OnUdpNotifyPropertyChanged; } private void OnUdpNotifyPropertyChanged(object sender, UdpNotifyEventArgs e) {
// 不接受属性变化通知 if (!IsAcceptDataFromSender) return;
// 不接受不同进程的通知 if (e.ProcessId != ProcessId) return;
// 本实例发出的通知也不接受(不同实例的GetHashCode()返回值肯定不同) if (this.GetHashCode() == e.HashCode) return;
// 不同类型的实例发出的通知也不接受 if (this.GetType().FullName != e.ClassType) return; SetValue(e.Value, e.PropertyName, e.PrivatePropertyName); } public void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } public void SetValue<T>(ref T field, T value, string propertyName, string privateName) { if (!EqualityComparer<T>.Default.Equals(field, value)) { field = value; this.OnPropertyChanged(propertyName); if (!IsSender) return; var e = new UdpNotifyEventArgs { Value = value, PropertyName = propertyName, PrivatePropertyName = privateName, ClassType = this.GetType().FullName, HashCode = this.GetHashCode(), ProcessId = UdpNotify.ProcessId }; _udpNotify.Send(e); } } /// <summary> /// 设置属性值 /// </summary> /// <param name="value">属性值</param> /// <param name="propertyName">属性名</param> /// <param name="privateName">与属性名对应的私有变量名称</param> public void SetValue(object value, string propertyName, string privateName) { var type = this.GetType(); var props = this.GetType().GetProperties(); var prop = props.FirstOrDefault(x => x.Name == propertyName); if (prop == null) return; if (prop.Name != propertyName) return; if (!prop.CanWrite) return; var bindings = BindingFlags.Instance | BindingFlags.GetField | BindingFlags.NonPublic | BindingFlags.ExactBinding; var field = type.GetField(privateName, bindings); if (field == null) return; var val = ParseDataByType(value, prop.PropertyType); // 这里不能用prop.SetValue(this, val); // 因为这样会触发SetValue<T>方法, // 导致多执行一次OnPropertyChanged事件 field.SetValue(this, val); OnPropertyChanged(propertyName); } public object ParseDataByType(object data, Type type) { var str = data.ToString(); if (type == typeof(byte)) return byte.Parse(str); if (type == typeof(char)) return char.Parse(str); if (type == typeof(short)) return short.Parse(str); if (type == typeof(ushort)) return ushort.Parse(str); if (type == typeof(int)) return int.Parse(str); if (type == typeof(uint)) return uint.Parse(str); if (type == typeof(long)) return long.Parse(str); if (type == typeof(ulong)) return ulong.Parse(str); if (type == typeof(float)) return float.Parse(str); if (type == typeof(double)) return double.Parse(str); if (type == typeof(decimal)) return decimal.Parse(str); if (type == typeof(string)) return str; if (type == typeof(DateTime)) return DateTime.Parse(str); return data; } public void Dispose() { _udpNotify.UdpNotifyPropertyChanged -= OnUdpNotifyPropertyChanged; } } }
这里需要注意,UdpNotify需要声明为静态的,并且构造函数中只有当_udpNotify为空时才进行初始化,UdpNotifyPropertyChanged事件每个实例都会注册一次,这样_udpNotify就会有多个UdpNotifyPropertyChanged事件,形成多播。多播是这里实现实例间属性值变化通知的核心。
IsSender 属性的作用是,决定该实例属性值的变化是否会向外传播。
IsAcceptDataFromSender属性,决定当前实例是否接收其他实例传播过来的属性值变化通知。
ProcessId为当前实例所在的进程,本模式仅支持进程内的实例属性值变化通知。通过ProcessId限定不可接受其他进程的通知。
也要注意一下_udpNotify的UdpNotifyPropertyChanged事件的实现OnUdpNotifyPropertyChanged,在OnUdpNotifyPropertyChanged需要拦截一些不合法的通知,
合法的通知通过SetValue(object value, string propertyName, string privateName)方法传播到其他实例。
5. 基于NotifyPropertyChangeBase实现数据模型类
using NotifyPropertyChangedBetweenInstance.Common; namespace NotifyPropertyChangedBetweenInstance.ViewModels { public class TestViewModel : NotifyPropertyChangeBase { private int _id = 0; public int Id { get => _id; set => SetValue(ref _id, value, nameof(Id), nameof(_id)); } private string _code = ""; public string Code { get => _code; set => SetValue(ref _code, value, nameof(Code), nameof(_code)); } } }
这里要注意setter,setter通过基类里的SetValue<T>方法通知属性值变化。并在SetValue<T>中通过_udpNotify.Send发送到所有实例。
6. 实例程序
这里仅仅贴出MainWindow的实现,两个UserControl的实现下载源码看吧。
MainWindow.xaml:
<Window x:Class="NotifyPropertyChangedBetweenInstance.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:NotifyPropertyChangedBetweenInstance" xmlns:controls="clr-namespace:NotifyPropertyChangedBetweenInstance.mqyControls" mc:Ignorable="d" Title="MainWindow" Height="240" Width="450"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*"/> <RowDefinition Height="*"/> <RowDefinition Height="60"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <GroupBox Header="Grid01可以收到通知哦" Grid.Row="0" Grid.Column="0"> <controls:NotifyControl HorizontalAlignment="Left"/> </GroupBox> <GroupBox Header="Grid02也可以收到通知哦" Grid.Row="0" Grid.Column="1"> <controls:NotifyControl HorizontalAlignment="Left"/> </GroupBox> <GroupBox Header="Grid10不可以可以收到通知哦" Grid.Row="1" Grid.Column="0"> <controls:NotNotifyControl HorizontalAlignment="Left"/> </GroupBox> <GroupBox Header="Grid11不可以可以收到通知哦" Grid.Row="1" Grid.Column="1"> <controls:NotNotifyControl HorizontalAlignment="Left"/> </GroupBox> <StackPanel Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" Orientation="Horizontal" HorizontalAlignment="Center"> <Button Content="Notify Id" Width="100" Height="35" Margin="10,0" Click="ChangeId_OnClick"/> <Button Content="Notify Code" Width="100" Height="35" Margin="10,0" Click="ChangeCode_OnClick"/> <Button Content="Not Notify Code" Width="100" Height="35" Margin="10,0" Click="NotNotify_Click"/> </StackPanel> </Grid> </Window>
MainWindow.xaml.cs:
using NotifyPropertyChangedBetweenInstance.ViewModels; using System; using System.Windows; namespace NotifyPropertyChangedBetweenInstance { /// <summary> /// MainWindow.xaml 的交互逻辑 /// </summary> public partial class MainWindow : Window { private Random _random; public MainWindow() { InitializeComponent(); _random = new Random(Guid.NewGuid().GetHashCode()); } private void ChangeId_OnClick(object sender, RoutedEventArgs e) { var n = _random.Next(1, 10000); Title = $"New TestViewModel.Id:{n}"; var model = new TestViewModel() { IsSender = true }; model.Id = n; } private void ChangeCode_OnClick(object sender, RoutedEventArgs e) { var str = Convert.ToBase64String(Guid.NewGuid().ToByteArray(), 0); Title = $"New TestViewModel.Code:{str}"; var model = new TestViewModel() { IsSender = true }; model.Code = str; } private void NotNotify_Click(object sender, RoutedEventArgs e) { var str = Convert.ToBase64String(Guid.NewGuid().ToByteArray(), 0); Title = $"New TestViewModel.Code:{str}"; var model = new TestViewModel() { IsSender = false }; model.Code = str; } } }
7. 有图有真相
看看实现效果吧。