原创 夏群林 2025.8.4
显示弹出消息,Microsoft.Maui.Controls 命名空间下的 Page 类,提供了 DisplayAlert / DisplayActionSheet / DisplayPromptAsync 三种方法,满足一般的对话交互需要,但必须点击类似 "OK" / "Cancel" 的按钮关闭窗口才能结束对话。也可以自定义一个 ContenPage 页,有点麻烦,除非特殊要求,否则没必要。如果单纯显示消息,发送后不管,官方提供了 Snackbar / Toast,会在可配置的持续时间后被消除。
但在我的应用中,发现 Snackbar / Toast 不好用,经常显示反应不及时。更麻烦的是,当有系列的提示密集发送,主体流程早已结束,消息还在慢吞吞的往外嘣,体验很不好。
于是我自己做了一个 NoticeView, 作为 MAUI 应用中的轻量级通知组件,核心目标是实现多优先级消息的有序管理与灵活显示,其解决的关键问题,即后续消息到来,前面不太重要的消息,要让路,要清空。这样,保证消息的及时显示。
具体需求包括:
- 支持多级优先级(Low/Medium/High/Critical),高优先级消息需“插队”优先显示,低优先级消息无条件让路;
- 每条消息需保证最小显示时长(避免闪显),无后续消息时按默认时长显示;
- 自动清理低优先级消息,限制队列最大长度,防止内存溢出;
- 支持多线程环境下的消息发送,确保队列操作安全;
- 提供手动清空功能,资源释放时自动清理,避免内存泄漏。
一、架构设计
1. 核心组件
NoticePriority:枚举定义优先级,Critical 为最高级,权压一切,比如,用Critical级确保清空所有消息。- public enum NoticePriority { Low, Medium, High, Critical }
- public static void ClearNotice() => DisplayNotice(string.Empty, NoticePriority.Critical);
复制代码 NoticeMessage:消息载体,包含内容(Value)和优先级(Priority);- public class NoticeMessage(string value, NoticePriority priority = NoticePriority.Medium)
- {
- public string Value { get; } = value;
- public NoticePriority Priority { get; } = priority;
- }
复制代码 NoticeDisplayOptions:配置类,控制显示时长(MinDisplayDuration/DisplayDuration)、队列最大长度(MaxQueueLength)。提供了默认显示样式,包括字体、颜色、旋转、阴影,我喜欢浮雕斜上如同惊鸿一瞥的效果,作为默认配置。各人可自行定义。- public class NoticeDisplayOptions
- {
- public TimeSpan DisplayDuration { get; set; } = TimeSpan.FromSeconds(3);
- public TimeSpan MinDisplayDuration { get; set; } = TimeSpan.FromMilliseconds(500);
- public int MaxQueueLength { get; set; } = 50;
-
- public Color FontColor { get; set; } = Colors.DarkOrchid;
- public float FontSize { get; set; } = 16F;
- public float RotationAngle { get; set; } = -20;
- public bool HasShadow { get; set; } = true;
- }
复制代码 QueuedNotice:内部队列消息类,封装优先级、入队序号(Order)和内容,实现 IComparable 接口用于排序;- private class QueuedNotice(NoticePriority priority, int order, string message) : IComparable<QueuedNotice>
- {
- public NoticePriority Priority { get; } = priority;
- public int Order { get; } = order;
- public string Message { get; } = message;
- public int CompareTo(QueuedNotice? other)
- {
- if (other is null) return 1;
- // 优先级顺序:Critical > High > Medium > Low
- int priorityCompare = other.Priority.CompareTo(Priority);
- return priorityCompare != 0 ? priorityCompare : Order.CompareTo(other.Order);
- }
- }
复制代码 NoticeView:核心控件,继承 GraphicsView,实现 IDrawable,用于绘制UI;和 IDisposable,便于资源释放。NoticeView 负责消息接收、队列管理、显示控制。 也提供了发送消息和清除消息的静态方法。- public partial class NoticeView : GraphicsView, IDrawable, IDisposable
- {
- private readonly NoticeDisplayOptions _options;
- private readonly ConcurrentDictionary<int, QueuedNotice> _messageQueue = new();
- private readonly Lock _queueSync = new();
- private QueuedNotice? _currentMessage;
- private DateTimeOffset _currentMessageStartTime;
- private int _messageOrder = 0;
- private bool _isTimerRunning;
- private IDispatcherTimer? _timer;
- public NoticeView(NoticeDisplayOptions? options = null)
- {
- _options = options ?? new NoticeDisplayOptions();
- Drawable = this;
- VerticalOptions = LayoutOptions.Fill;
- HorizontalOptions = LayoutOptions.Fill;
- InputTransparent = true;
- WeakReferenceMessenger.Default.Register<NoticeMessage>(this, (_, m) => OnMessageArrive(m));
- }
-
- // ...
-
- }
复制代码 2. 整体架构图
- ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
- │ 外部调用者 │──┬──>│ NoticeMessage │──┬──>│ NoticeView │
- └─────────────────┘ │ └─────────────────┘ │ └────────┬────────┘
- │ │ │
- │ │ ▼
- │ │ ┌─────────────────┐
- │ └──>│ 消息队列管理 │
- │ └────────┬────────┘
- │ │
- └──────────────────────────────────────┘
- 发送消息
复制代码 二、关键实现与核心代码
1. 优先级与排序机制
通过自定义排序规则保证消息按“优先级降序+同优先级入队顺序升序”排列,确保高优先级消息先显示,同优先级消息按发送顺序显示。
实现细节:
- 自定义 QueuedNotice 类并实现 IComparable 接口,排序规则:
- 先比较优先级(Critical > High > Medium > Low);
- 优先级相同时,比较入队序号(Order),序号越小(入队越早)越优先。
- 入队序号通过 Interlocked.Increment 生成,保证多线程环境下的原子性和唯一性,避免序号冲突。
- // 生成唯一入队序号(多线程安全)
- int enqueueOrder = Interlocked.Increment(ref _messageOrder);
复制代码 设计考量:
- 用 IComparable 封装排序逻辑,避免分散在业务代码中,提高可维护性;
- 原子操作生成序号,解决多线程并发下的序号重复问题,确保同优先级消息的顺序性。
2. 队列管理策略
通过线程安全容器存储消息,自动清理低优先级消息,控制队列长度,确保高优先级消息“无障碍”显示。
实现细节:
- 采用 ConcurrentDictionary 作为队列容器,以入队序号(Order)为键,支持并发读写,减少锁竞争;
- 新消息入队时,根据优先级清理低优先级消息:
- 非 Critical 级:移除队列中所有优先级低于新消息的消息;
- Critical 级:直接清空队列+清除当前显示的消息(权压一切);
- 队列长度超过 MaxQueueLength 时,循环移除“最低优先级中最早入队的消息”,避免队列无限增长。
- private void OnMessageArrive(NoticeMessage message)
- {
- int enqueueOrder = Interlocked.Increment(ref _messageOrder);
- var enqueueNotice = new QueuedNotice(message.Priority, enqueueOrder, message.Value);
- bool needImmediateSwitch = false;
- lock (_queueSync) // 加锁保证原子性
- {
- if (message.Priority == NoticePriority.Critical)
- {
- // Critical级:清空队列+清除当前消息,直接抢占显示
- _messageQueue.Clear();
- _currentMessage = null;
- needImmediateSwitch = true;
- }
- else
- {
- // 非Critical级:移除队列中所有低优先级消息
- var lowerPriorityItems = _messageQueue.Values
- .Where(m => m.Priority < message.Priority)
- .ToList();
- foreach (var item in lowerPriorityItems)
- _ = _messageQueue.TryRemove(item.Order, out _);
- // 关键:当前显示的消息若优先级更低,立即清除并标记切换
- if (_currentMessage != null && _currentMessage.Priority < message.Priority)
- {
- _messageQueue.TryRemove(_currentMessage.Order, out _);
- _currentMessage = null;
- needImmediateSwitch = true;
- }
- }
- // 队列长度控制:超过上限时,移除最低优先级中最旧的消息
- while (_messageQueue.Count >= _options.MaxQueueLength)
- {
- var lowestPriority = _messageQueue.Values.Min(m => m.Priority);
- var oldestLow = _messageQueue.Values
- .Where(m => m.Priority == lowestPriority)
- .OrderBy(m => m.Order)
- .FirstOrDefault();
- if (oldestLow != null)
- _messageQueue.TryRemove(oldestLow.Order, out _);
- else
- break; // 极端情况防止死循环
- }
- _messageQueue.TryAdd(enqueueNotice.Order, enqueueNotice);
- }
- // 高优先级消息立即切换显示,无需等待当前消息时长
- if (message.Priority == NoticePriority.Critical || needImmediateSwitch)
- MainThread.BeginInvokeOnMainThread(SwitchToNextMessage);
- else
- MainThread.BeginInvokeOnMainThread(UpdateDisplay);
- }
复制代码 设计考量:
- 用 ConcurrentDictionary 减少简单操作,如单条添加 / 移除的锁竞争,提高并发性能;
- 复合操作,如批量清理+添加,用 lock 保证原子性,避免数据不一致;
- Critical 级消息直接清空队列和当前消息,确保“最高优先级”的绝对权威性;
- 队列长度控制优先移除“最低优先级中最旧的消息”,平衡了优先级和时效性。
3. 显示与切换逻辑
通过定时器监控消息显示时长,根据“是否有后续消息”动态调整显示时长,高优先级消息可强制打断当前消息显示。
实现细节:
- 定时器采用 DispatcherTimer,绑定UI线程,间隔100ms高频检查,确保时长判断精准;
- 显示时长规则: 有后续消息时,当前消息至少显示 MinDisplayDuration,避免闪显; 无后续消息时,显示时长为 MinDisplayDuration 与 DisplayDuration 的最大值,保证用户能看清;
- 切换逻辑:当满足时长条件或收到更高优先级消息时,移除当前消息,显示队列中优先级最高的下一条消息。
- private void Timer_Tick(object? sender, EventArgs e)
- {
- if (_currentMessage == null)
- {
- StopTimer();
- return;
- }
- // 计算当前消息已显示时长(从开始显示时起算)
- var elapsed = DateTimeOffset.Now - _currentMessageStartTime;
- bool hasNextMessage;
- lock (_queueSync)
- {
- // 检查是否有除当前消息外的其他消息
- hasNextMessage = _messageQueue.Count > (_currentMessage != null ? 1 : 0);
- }
- // 动态计算最大显示时长
- var maxDuration = hasNextMessage
- ? _options.MinDisplayDuration
- : TimeSpan.FromMilliseconds(Math.Max(
- _options.MinDisplayDuration.TotalMilliseconds,
- _options.DisplayDuration.TotalMilliseconds));
- // 满足时长条件则切换消息
- if (elapsed >= maxDuration)
- MainThread.BeginInvokeOnMainThread(SwitchToNextMessage);
- }
- // 切换到下一条消息
- private void SwitchToNextMessage()
- {
- lock (_queueSync)
- {
- // 移除当前显示的消息
- if (_currentMessage != null)
- _messageQueue.TryRemove(_currentMessage.Order, out _);
- // 显示下一条消息(队列中优先级最高的)
- var nextNotice = !_messageQueue.IsEmpty
- ? _messageQueue.Values.OrderBy(m => m).FirstOrDefault()
- : null;
- if (nextNotice != null)
- {
- _currentMessage = nextNotice;
- _currentMessageStartTime = DateTimeOffset.Now; // 从显示时开始计时
- Notice = nextNotice.Message;
- Invalidate(); // 触发UI重绘
- }
- else
- {
- ClearCurrentMessage(); // 队列空则清除显示
- }
- }
- }
复制代码 设计考量:
- _currentMessageStartTime 仅在消息开始显示时赋值,确保计时起点与用户可见时间一致;
- 高频定时器(100ms)减少时长判断的延迟,提升用户体验;
- 动态调整显示时长(区分有/无后续消息),既避免消息闪显,又防止无后续消息时显示过短。
4. 线程安全与资源管理
通过锁机制和线程隔离保证多线程安全,通过 IDisposable 接口和手动清理方法确保资源释放。
实现细节:
- 线程安全:
- 简单操作(单条消息的添加/移除)依赖 ConcurrentDictionary 的线程安全特性;
- 复合操作(批量清理、队列长度控制)用 lock (_queueSync) 保证原子性;
- 所有UI操作(如刷新显示、切换消息)通过 MainThread.BeginInvokeOnMainThread 限制在主线程,避免跨线程异常。
- 资源管理:
- 实现 IDisposable 接口,释放时注销消息订阅、停止定时器、清空队列;
- 提供 ClearQueue 手动清空方法,支持主动清理;
- ClearNotice 方法通过发送 Critical 级空消息,利用其“清空一切”特性快速清理。
- public void Dispose()
- {
- // 注销消息订阅,避免内存泄漏
- WeakReferenceMessenger.Default.UnregisterAll(this);
- StopTimer(); // 停止定时器
- ClearQueue(); // 清空队列和当前消息
- GC.SuppressFinalize(this);
- }
- // 手动清空队列
- public void ClearQueue()
- {
- lock (_queueSync)
- {
- _messageQueue.Clear();
- ClearCurrentMessage();
- }
- }
- // 清空通知(利用Critical级特性)
- public static void ClearNotice() => DisplayNotice(string.Empty, NoticePriority.Critical);
复制代码 设计考量:
- 最小化锁范围(仅复合操作加锁),平衡线程安全和性能;
- 消息订阅使用弱引用 WeakReferenceMessenger,配合 Dispose 注销,避免组件销毁后仍接收消息导致的内存泄漏;
- ClearNotice 复用 Critical 级消息的清理逻辑,减少代码冗余,同时保证清理彻底性。
三、使用示例
使用很方便。在需要显示的页面,放一个 NoticeView 实例即可:- public partial class SearchPage : ContentPage
- {
- public SearchPage(SearchViewModel viewModel)
- {
- InitializeComponent();
- this.contentPageLayout.Children.Add(new NoticeView() { ZIndex = 3, InputTransparent = true });
- }
-
- // ...
- }
复制代码 并且,可在多个显示页面放置 NoticeView 实例。
在任何需要的地方调用:- NoticeView.DisplayNotice($"Neither found nor suggested",NoticePriority.High);
复制代码 四、得意之处
通过“优先级排序+队列动态清理+即时切换+线程安全”的设计,NoticeView 实现了高优先级消息优先显示、低优先级消息自动让路的核心需求。其设计亮点在于:
- 用 IComparable 和原子序号保证了多线程下的消息顺序;
- 区分简单/复合操作的线程安全策略,平衡了性能和安全性;
- 动态时长控制和强制切换机制,兼顾了用户体验和优先级权威性;
- 完善的资源清理机制,避免了内存泄漏风险。
该组件可直接集成到 MAUI 应用中,支持自定义样式和时长,适用于各类轻量级通知场景,如操作提示、状态更新等。
本方案源代码开源,按照 MIT 协议许可。地址 xiaql/Zhally.Toolkit: Practices on specific tool kit in MAUI。
来源:豆瓜网用户自行投稿发布,如果侵权,请联系站长删除 |