原创 夏群林
显示弹出消息,Microsoft.Maui.Controls 命名空间下的 Page 类,提供了 DisplayAlert / DisplayActionSheet / DisplayPromptAsync 三种方法,满足一般的对话交互需要,但必须点击类似 "OK" / "Cancel" 的按钮关闭窗口才能结束对话。也可以自定义一个 ContenPage 页,有点麻烦,除非特殊要求,否则没必要。如果单纯显示消息,发送后不管,官方提供了 Snackbar / Toast,会在可配置的持续时间后被消除。
但在我的应用中,发现 Snackbar / Toast 不好用,经常显示反应不及时。更麻烦的是,当有系列的提示密集发送,主体流程早已结束,消息还在慢吞吞的往外嘣,体验很不好。
于是我自己做了一个 NoticeView, 作为 MAUI 应用中的轻量级通知组件,核心目标是实现多优先级消息的有序管理与灵活显示,其解决的关键问题,即后续消息到来,前面不太重要的消息,要让路,要清空。这样,保证消息的及时显示。
具体需求包括:
- 支持多级优先级(Low/Medium/High/Critical),高优先级消息需"插队"优先显示,低优先级消息无条件让路;
- 每条消息需保证最小显示时长(避免闪显),无后续消息时按默认时长显示;
- 自动清理低优先级消息,限制队列最大长度,防止内存溢出;
- 支持多线程环境下的消息发送,确保队列操作安全;
- 提供手动清空功能,资源释放时自动清理,避免内存泄漏。
一、架构设计
1. 核心组件
NoticePriority:枚举定义优先级,Critical 为最高级,权压一切,比如,用Critical级确保清空所有消息。
c#
public enum NoticePriority { Low, Medium, High, Critical }
public static void ClearNotice() => DisplayNotice(string.Empty, NoticePriority.Critical);
NoticeMessage:消息载体,包含内容(Value)和优先级(Priority);
c#
public class NoticeMessage(string value, NoticePriority priority = NoticePriority.Medium)
{
public string Value { get; } = value;
public NoticePriority Priority { get; } = priority;
}
NoticeDisplayOptions:配置类,控制显示时长(MinDisplayDuration/DisplayDuration)、队列最大长度(MaxQueueLength)。提供了默认显示样式,包括字体、颜色、旋转、阴影,我喜欢浮雕斜上如同惊鸿一瞥的效果,作为默认配置。各人可自行定义。
c#
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 接口用于排序;
c#
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 负责消息接收、队列管理、显示控制。 也提供了发送消息和清除消息的静态方法。
c#
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. 整体架构图
plaintext
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 外部调用者 │──┬──>│ NoticeMessage │──┬──>│ NoticeView │
└─────────────────┘ │ └─────────────────┘ │ └────────┬────────┘
│ │ │
│ │ ▼
│ │ ┌─────────────────┐
│ └──>│ 消息队列管理 │
│ └────────┬────────┘
│ │
└──────────────────────────────────────┘
发送消息
二、关键实现与核心代码
1. 优先级与排序机制
通过自定义排序规则保证消息按"优先级降序+同优先级入队顺序升序"排列,确保高优先级消息先显示,同优先级消息按发送顺序显示。
实现细节:
- 自定义
QueuedNotice
类并实现IComparable<QueuedNotice>
接口,排序规则:- 先比较优先级(Critical > High > Medium > Low);
- 优先级相同时,比较入队序号(Order),序号越小(入队越早)越优先。
- 入队序号通过
Interlocked.Increment
生成,保证多线程环境下的原子性和唯一性,避免序号冲突。
csharp
// 生成唯一入队序号(多线程安全)
int enqueueOrder = Interlocked.Increment(ref _messageOrder);
设计考量:
- 用
IComparable
封装排序逻辑,避免分散在业务代码中,提高可维护性; - 原子操作生成序号,解决多线程并发下的序号重复问题,确保同优先级消息的顺序性。
2. 队列管理策略
通过线程安全容器存储消息,自动清理低优先级消息,控制队列长度,确保高优先级消息"无障碍"显示。
实现细节:
- 采用
ConcurrentDictionary<int, QueuedNotice>
作为队列容器,以入队序号(Order)为键,支持并发读写,减少锁竞争; - 新消息入队时,根据优先级清理低优先级消息:
- 非 Critical 级:移除队列中所有优先级低于新消息的消息;
- Critical 级:直接清空队列+清除当前显示的消息(权压一切);
- 队列长度超过
MaxQueueLength
时,循环移除"最低优先级中最早入队的消息",避免队列无限增长。
csharp
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
的最大值,保证用户能看清; - 切换逻辑:当满足时长条件或收到更高优先级消息时,移除当前消息,显示队列中优先级最高的下一条消息。
csharp
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 级空消息,利用其"清空一切"特性快速清理。
- 实现
csharp
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 实例即可:
c#
public partial class SearchPage : ContentPage
{
public SearchPage(SearchViewModel viewModel)
{
InitializeComponent();
this.contentPageLayout.Children.Add(new NoticeView() { ZIndex = 3, InputTransparent = true });
}
// ...
}
并且,可在多个显示页面放置 NoticeView 实例。
在任何需要的地方调用:
c#
NoticeView.DisplayNotice($"Neither found nor suggested",NoticePriority.High);
四、得意之处
通过"优先级排序+队列动态清理+即时切换+线程安全"的设计,NoticeView 实现了高优先级消息优先显示、低优先级消息自动让路的核心需求。其设计亮点在于:
- 用
IComparable
和原子序号保证了多线程下的消息顺序; - 区分简单/复合操作的线程安全策略,平衡了性能和安全性;
- 动态时长控制和强制切换机制,兼顾了用户体验和优先级权威性;
- 完善的资源清理机制,避免了内存泄漏风险。
该组件可直接集成到 MAUI 应用中,支持自定义样式和时长,适用于各类轻量级通知场景,如操作提示、状态更新等。
本方案源代码开源,按照 MIT 协议许可。我会放在地址 https://github.com/xiaql/Zhally.Toolkit,敬请关注。