8.1 ABP实时服务 - 通知系统
8.1.1 简介
在系统中,通知是用来告知用户特定事件的。ABP提供了一个基于实时通知的基础设施 pub/sub.
8.1.2 发送模式
有两种方法来发送通知给用户:
用户 订阅 一个特定的通知类型。当我们发布这个类型的通知时,该通知会被投递给所有的订阅用户。这就是 pub/sub 模式。
我们能直接的发送通知给目标用户。
8.1.3 通知类型
通知类型也有两种:
常规通知 是任意类型的通知。例如:”如果某个用户发送给我一个交友请求,那么通知我。”
实体通知 是被关联到一个指定的实体。”如果某个用户评论了这个照片,那么通知我。”,这是一个基于实体的通知,因为它被关联到一个特定的照片实体。用户可以得到对某些照片评论的通知,而不是所有照片的。
8.1.4 通知数据
通常来说通知中包含了通知数据。例如:”如果某个用户发送给我一个友好的请求,那么通知我。”,该通知可以有两个数据属性:发送者的用户名(那个用户发送了这个交友请求)和请求信息(用户发送请求的具体信息)。很显然,这个通知的数据类型是和通知类型紧密耦合的。不同的通知类型有不同的数据类型。
通知数据是可选的。有些通知不需要数据。ABP预定义了一些通知数据类型,这些类型适合于大多数情况下。MessageNotificationData 能够用于一些简单的消息,而 LocalizableMessageNotificationData 可以用于本地化和参数化的通知消息。在后面的部分我们会看到一些有用的例子。
8.1.5 通知的程度
ABP有5种程度的通知等级,它被定义在枚举类型 NotificationSeverity 中: Info, Success, Warn, Error 以及 Fatal。 默认是 Info。很显然,这个通知的数据类型是和通知类型紧密耦合的。不同的通知类型有不同的数据类型。
关于通知持久化
了解更多请看 Notification Store
8.1.6 订阅通知
INotificationSubscriptionManager 接口中提供了订阅通知的API,例如:
public class MyService : ITransientDependency
{
private readonly INotificationSubscriptionManager _notificationSubscriptionManager;
public MyService(INotificationSubscriptionManager notificationSubscriptionManager)
{
_notificationSubscriptionManager = notificationSubscriptionManager;
}
//订阅常规通知
public async Task Subscribe_SentFrendshipRequest(int? tenantId, long userId)
{
await _notificationSubscriptionManager.SubscribeAsync(tenantId, userId, "SentFrendshipRequest");
}
//订阅实体通知
public async Task Subscribe_CommentPhoto(int? tenantId, long userId, Guid photoId)
{
await _notificationSubscriptionManager.SubscribeAsync(tenantId, userId, "CommentPhoto", new EntityIdentifier(typeof(Photo), photoId));
}
}
首先,我们 注入 INotificationSubscriptionManager 接口。第一个方法是定义 常规通知,用户想得到某些用户发来的交友请求的通知。第二个方法是订阅与 特定实体(Photo) 有关的通知。用户想得到某些用户对特定的照片写了评论的通知。
每个通知的类型应该有唯一的名字(就像例子中的 SentFrendshipRequest 和 CommentPhoto 一样)。
INotificationSubscriptionManager 还有其它的方法来管理订阅,如: UnsubscribeAsync, IsSubscribedAsync, GetSubscriptionsAsync等等。
8.1.7 发布通知
INotificationPublisher 被用来发布通知,例如:
public class MyService : ITransientDependency
{
private readonly INotificationPublisher _notiticationPublisher;
public MyService(INotificationPublisher notiticationPublisher)
{
_notiticationPublisher = notiticationPublisher;
}
//发送常规通知给特定用户
public async Task Publish_SentFrendshipRequest(string senderUserName, string friendshipMessage, long targetUserId)
{
await _notiticationPublisher.PublishAsync("SentFrendshipRequest", new SentFrendshipRequestNotificationData(senderUserName, friendshipMessage), userIds: new[] { targetUserId });
}
//发送实体通知给特定用户
public async Task Publish_CommentPhoto(string commenterUserName, string comment, Guid photoId, long photoOwnerUserId)
{
await _notiticationPublisher.PublishAsync("CommentPhoto", new CommentPhotoNotificationData(commenterUserName, comment), new EntityIdentifier(typeof(Photo), photoId), userIds: new[] { photoOwnerUserId });
}
//发送常规通知给所有的订阅用户并制定通知的严重等级
public async Task Publish_LowDisk(int remainingDiskInMb)
{
//例如:"LowDiskWarningMessage" 英文内容是 -> "Attention! Only {remainingDiskInMb} MBs left on the disk!"
//注意,磁盘空间仅剩下{remainingDiskInMb} MBs
var data = new LocalizableMessageNotificationData(new LocalizableString("LowDiskWarningMessage", "MyLocalizationSourceName"));
data["remainingDiskInMb"] = remainingDiskInMb;
await _notiticationPublisher.PublishAsync("System.LowDisk", data, severity: NotificationSeverity.Warn);
}
}
在第一个例子中,我们对一个用户发布了一条通知。SentFrendshipRequestNotificationData 应该派生自 NotificationData 如下所示:
[Serializable]
public class SentFrendshipRequestNotificationData : NotificationData
{
public string SenderUserName { get; set; }
public string FriendshipMessage { get; set; }
public SentFrendshipRequestNotificationData(string senderUserName, string friendshipMessage)
{
SenderUserName = senderUserName;
FriendshipMessage = friendshipMessage;
}
}
在第二个例子中,我们发送了一个特定实体的通知给了特定的用户。通知数据类不需要被 序列化 (因为默认被序列化为JSON)。但是还是建议你将特性:Serializable 加在数据类上,因为你可能需要在应用之间移动通知,也可能在将来使用二进制序列化。此外,正如之前声明的那样,通知数据是可选的,而且对于所有的通知可能不是必须的。
注意:如果我们发布通知给特定的用户,那么他们不需要订阅那些通知。
在第三个例子中,我们没有定义一个专门的通知数据类。相反,直接使用了内置的基于字典类型的 LocalizableMessageNotificationData 类,并以 Warn 等级来发布通知。
LocalizableMessageNotificationData 能存储基于字典的任意数据(这是由于自定义通知数据类型是继承 NotificationData 类)。我们在本地化时使用 remainingDiskInMb 作为参数。本地化消息可以包含这些参数(就像例子中的”Attention! Only {remainingDiskInMb} MBs left on the disk!”)。我们将会在客户端看到如何本地化。
8.1.8 用户通知管理
IUserNotificationManager 被用来管理用户通知。有这些方法来为用户 get, update 或 delete 通知。你可以使用它们为你的应用程序来准备一个通知列表页面。
8.1.9 实时通知
当你使用 IUserNotificationManager 来查询通知时,通常我们是想实时推送通知到客户端。
通知系统使用 IRealTimeNotifier 接口发送实时通知给用户。任何类型的实时通信系统都能实现它。它已经被实现在一个独立的 SignalR 包中。在启动模板中已经安装了SignalR。详情请参考集成SignalR。
注意:在后台作业中,通信系统异步调用 IRealTimeNotifier。所以,通知可能会延迟一小会才会被发送。
8.1.10 客户端
当实时通知被接受的时候,ABP在客户端触发了一个 全局事件 。你可以像下面一样注册它来获取通知:
abp.event.on('abp.notifications.received', function (userNotification) {
console.log(userNotification);
});
每次接受到实时通知 abp.notifications.received 事件会被触发。你能够想上面展示的那样注册这个事件来获取通知。详情请参考事件总线。下面是一个传入的JSON格式的”System.LowDisk”示例:
{
"userId": 2,
"state": 0,
"notification": {
"notificationName": "System.LowDisk",
"data": {
"message": {
"sourceName": "MyLocalizationSourceName",
"name": "LowDiskWarningMessage"
},
"type": "Abp.Notifications.LocalizableMessageNotificationData",
"properties": {
"remainingDiskInMb": "42"
}
},
"entityType": null,
"entityTypeName": null,
"entityId": null,
"severity": 0,
"creationTime": "2016-02-09T17:03:32.13",
"id": "0263d581-3d8a-476b-8e16-4f6a6f10a632"
},
"id": "4a546baf-bf17-4924-b993-32e420a8d468"
}
在这个对象中:
userId:当前用户Id。通常你不需要知道这个,因为你知道那个是当前用户。
state:枚举类型 UserNotificationState 的值。 0:Unread,1:Read。
notification:通知详细信息:
notificationName: 通知的唯一名字(发布通知时使用相同的值)。
data:通知数据。在上面例子中,我们使用了 LocalizableMessageNotificationData (在之前的例子中我们使用它来发布的)。
message: 本地化信息。在UI端,我们可以使用 sourceName 和 name 来本地化信息。
type:通知数据类型。类型的全名称,包含名称空间。当处理这个通知数据的时候,我们可以检查这个类型。
properties:自定义属的基于字典类型的属性。
entityType,entityTypeName 和 entityId:实体信息,如果这是一个与实体相关的通知。
severity:枚举类型 NotificationSeverity 的值。0: Info, 1: Success, 2: Warn, 3: Error, 4: Fatal。
creationTime:表示通知被创建的时间。
id:通知的id。
id:用户通知id。
当然你不会去记录这个通知。你可以使用通知数据来显示通知信息给用户。例如:
abp.event.on('abp.notifications.received', function (userNotification) {
if (userNotification.notification.data.type === 'Abp.Notifications.LocalizableMessageNotificationData') {
var localizedText = abp.localization.localize(
userNotification.notification.data.message.name,
userNotification.notification.data.message.sourceName
);
$.each(userNotification.notification.data.properties, function (key, value) {
localizedText = localizedText.replace('{' + key + '}', value);
});
alert('New localized notification: ' + localizedText);
} else if (userNotification.notification.data.type === 'Abp.Notifications.MessageNotificationData') {
alert('New simple notification: ' + userNotification.notification.data.message);
}
});
为了能够处理通知数据,我们应该检查数据类型。这个简单的例子展示的是从通知数据中取得消息。对于本地化消息(LocalizableMessageNotificationData),我们要本地化该消息并替换掉参数。对于简单消息(MessageNotificationData),我们直接的取得该消息。当然,在一个真实的项目中,我们不会使用alert函数。我们会使用 abp.notify API来展示漂亮的UI通知。
如果你需要实现上面所展示的逻辑,这里有一个更简单且可伸缩性的方法。当推送通知被接收到的时候,你可以仅使用一行代码来显示 UI通知。
abp.event.on('abp.notifications.received', function (userNotification) {
abp.notifications.showUiNotifyForUserNotification(userNotification);
});
下面展示的是一个 UI 通知 (上面System.LowDisk示例)。
它对于内置的通知数据类型(LocalizableMessageNotificationData 和 MessageNotificationData)可以很好的工作。如果你有自定义的通知数据类型,那么你应该像下面一样使用格式化器来注册这个数据:
abp.notifications.messageFormatters['MyProject.MyNotificationDataType'] = function(userNotification) {
return ...; //在这是实现格式化数据的逻辑并返回消息
};
因此,showUiNotifyForUserNotification 能对你的数据类型来创建并显示该消息。如果你仅需要格式化消息,你可以直接的使用 abp.notifications.getFormattedMessageFromUserNotification(userNotification),这是由showUiNotifyForUserNotification内部使用的。
当你接受到推送通知的时候,启动模板已经包含了显示UI通知的代码。
8.1.11 存储通知
通知系统使用 INotificationStore 来持久化通知。为了使通知系统正确的工作,我们应该实现这个接口。你可以自己实现它,或者使用已经实现了该接口的 module-zero。
8.1.12 通知定义
使用之前,你不需要定义一个通知。你可以使用任何的没有被定义过的通知名字。但是,定义通知名字可以给你带来额外的好处。例如:你可能需要在你的应用程序中调研所有的通知。在这种情况下,我们需要为我们模块 通知提供器(notification provider) ,如下所示:
public class MyAppNotificationProvider : NotificationProvider
{
public override void SetNotifications(INotificationDefinitionContext context)
{
context.Manager.Add(
new NotificationDefinition(
"App.NewUserRegistered",
displayName: new LocalizableString("NewUserRegisteredNotificationDefinition", "MyLocalizationSourceName"),
permissionDependency: new SimplePermissionDependency("App.Pages.UserManagement")
)
);
}
}
App.NewUserRegistered 是通知的唯一名字。我们定义了一个本地化的 displayName(当我们订阅了该通知时,我们可以在UI上显示它)。最后,我们声明了 App.Pages.UserManagement 权限,只有当用户具有该权限的时候,该通知才会显示给用户。
当然这里还有其它一些参数。你可以在代码中研究它们。对于通知定义只有通知的名字是必须的。
在你定义了如上所述的通知提供器后,我们应该在模块的 PreInitialize 方法中注册它。如下所示:
public class AbpZeroTemplateCoreModule : AbpModule
{
public override void PreInitialize()
{
Configuration.Notifications.Providers.Add<MyAppNotificationProvider>();
}
//...
}
最后,你可以在你的应用程序中注入并使用 INotificationDefinitionManager 接口来获取通知定义。