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. 有图有真相

        看看实现效果吧。

 

源码

 

posted @ 2019-09-10 13:15  stonemqy  阅读(428)  评论(0编辑  收藏  举报