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

            看看实现效果吧。

    源码

  • 相关阅读:
    Leetcode 214. Shortest Palindrome
    Leetcode 5. Longest Palindromic Substring
    windows环境下MySQL-5.7.12-winx64下载安装与配置
    随机森林(Random Forests)
    机器学习基石笔记3——在何时可以使用机器学习(3)
    Linux服务器配置---ssh配置
    Linux基础命令---more
    Linux服务器---流量监控ntop
    Linux基础命令---gunzip
    Linux服务器---ssh登录
  • 原文地址:https://www.cnblogs.com/stonemqy/p/11496964.html
Copyright © 2020-2023  润新知