位置无关的消息服务






4.64/5 (5投票s)
位置无关的消息服务
在开发应用程序时,显然在执行某些任务时保持一致性是明智的,从而避免违反 DRY 原则。一个例子是显示通用对话框。但请注意,如果您认为这篇博文仅仅是关于一个抽象的对话框系统,那么请三思。Calcium 确实提供了一个通用的对话框系统,但它也允许我们在任何 WCF 调用中从服务器向用户显示对话框!此外,它还允许我们在客户端和服务器上使用相同的 API。这意味着我们可以直接与用户交互,而无需在客户端解释 WCF 调用的结果。它有效地缩小了层级差距。
在这篇博文中,我们首先将讨论客户端消息服务,然后检查它如何从服务器端利用以提供位置无关性。
因此,显然,开发团队的每个成员都为简单的任务(例如向用户提问一个封闭式问题,如“是/否”问题框)创建自己的对话框是不明智的。如果我们决定在所有对话框中更改标题,那么当对话框分散在项目中的时候,这将变得相当困难。同样,如果我们希望将应用程序从 WPF 移植到 Silverlight,甚至移植到命令行界面(如 Powershell 或移动应用程序),能够为任何给定的场景替换实现将非常方便。显然,一个抽象的系统是必要的。
Calcium 开箱即用地提供了多种消息服务实现。有一个适用于 WPF 的客户端实现,另一个适用于命令行驱动应用程序的客户端实现,还有一个服务器端实现,它通过回调将消息发送回客户端,并利用客户端的 IMessageService
实现。
消息服务有各种重载,允许指定标题和消息。
让我们看一下 IMessageService
接口和客户端实现。
图示:IMessageService
允许我们以 UI 无关的方式与用户进行交互。
消息服务允许指定消息重要性级别。这允许用户设置一个阈值噪音级别。如果 MessageImportance
低于用户的偏好,则用户将不会被消息打扰。在 Calcium 的后续版本中,我们将看到一个首选项服务,用于指定首选级别。
图示:MessageService
和 CommandLineMessageService
都重写了 MessageServiceBase
的 ShowCustomDialog
方法,以适应它们各自的环境。
通过仅仅重写 ShowCustomDialog
方法来实现变化,这使得对 MessageServiceBase
类进行测试变得非常容易。
我们的客户端 WPF 实现通过 ShowCustomDialog
方法处理所有消息请求,如下面的摘录所示。
/// <summary>
/// WPF implementation of the <see cref="IMessageService"/>.
/// </summary>
public class MessageService : MessageServiceBase
{
public override MessageResult ShowCustomDialog(string message, string caption,
MessageButton messageButton, MessageImage messageImage,
MessageImportance? importanceThreshold, string details)
{
/* If the importance threshold has been specified
* and it's less than the minimum level required (the filter level)
* then we don't show the message. */
if (importanceThreshold.HasValue && importanceThreshold.Value < MinumumImportance)
{
return MessageResult.OK;
}
if (MainWindow.Dispatcher.CheckAccess())
{ /* We are on the UI thread, and hence no need to invoke the call.*/
var messageBoxResult = MessageBox.Show(MainWindow, message, caption,
messageButton.TranslateToMessageBoxButton(),
messageImage.TranslateToMessageBoxButton());
return messageBoxResult.TranslateToMessageBoxResult();
}
MessageResult result = MessageResult.OK; /* Satisfy compiler with default value. */
MainWindow.Dispatcher.Invoke((ThreadStart)delegate
{
var messageBoxResult = MessageBox.Show(MainWindow, message, caption,
messageButton.TranslateToMessageBoxButton(),
messageImage.TranslateToMessageBoxButton());
result = messageBoxResult.TranslateToMessageBoxResult();
});
return result;
}
static Window MainWindow
{
get
{
return UnitySingleton.Container.Resolve<IMainWindow>() as Window;
}
}
}
我创建了各种扩展方法,用于在 WPF 原生枚举 MessageBoxButton
、MessageBoxImage
和 MessageBoxResult
之间进行转换。为什么要费这么大力气?乍一看,这似乎是一种反模式。实际上,原因很好:WPF 和 Silverlight 在这些枚举上存在差异,这使得我们能够同时处理两者而无需重复。
我正在考虑扩展该服务以支持消息详细信息,并可能通过引用类型 Message
参数来减少重载的数量。另一个改进将是实现一个“不要再显示”复选框系统。我将其留给未来的版本。我甚至可能会使用 Karl Shifflet 出色的 Common TaskDialog 项目 此处(在 Karl 的许可下)。
服务器端消息服务
我们已经了解了 IMessageService
的基本客户端实现,但现在事情将变得更加有趣。使用 Calcium,我们能够以相同的方式在客户端和服务器上使用 IMessageService
。为了实现这一点,我们使用 WCF 自定义标头和双工服务回调。
演示应用程序包含一个名为 MessageServiceDemoModule
的模块。此模块的视图包含一个按钮,当 CommunicationService
与服务器建立连接时,该按钮将启用。
这是 WCF 服务中的代码,我们可以像这样异步执行它,并在客户端向用户显示对话框。请记住,此代码在服务器端执行!
public void DemonstrateMessageService()
{
var messageService = UnitySingleton.Container.Resolve<IMessageService>();
ThreadPool.QueueUserWorkItem(delegate
{
messageService.ShowMessage(
"This is a synchronous message invoked from the server." +
"The service method is still executing and waiting for your response.",
"DemoService", MessageImportance.High);
bool response1 = messageService.AskYesNoQuestion(
"This is a question invoked from the server. Select either Yes or No.",
"DemoService");
string responseString = response1 ? "Yes" : "No";
messageService.ShowMessage(string.Format(
"You selected {0} as a response to the last question.", responseString),
MessageImportance.High);
});
}
}
在上文摘录中,我们从 Unity 容器中检索 IMessageService
实例。这与我们在客户端执行的方式相同!不受位置限制使得我们更容易地移动业务逻辑。我将消息服务调用放在 QueueUserWorkItem
委托中,只是为了演示消息服务的独立性,即 WCF 调用几乎立即返回,但子线程将继续在后台工作,并且仍然有能力与用户通信。如果不使用子线程与用户通信,如果用户响应不够快,我们可能会遇到超时。请注意,我们必须在服务调用完成之前从 Unity 容器中检索消息服务。否则将引发异常,因为服务调用的 OperationContext
将不再存在。
这一切是如何工作的?当我们 [需要] 服务通道时,我们使用 IChannelManager
实例来检索一个实例。
我在几篇文章中讨论过 IChannelManager
,特别是 这里 和 这里。
为了支持从任何 WCF 服务调用识别应用程序实例,我们将一个自定义标头放入我们创建的每个服务通道中,如下面的 ServiceManagerSingleton
和 InstanceIdHeader
类的摘录所示。
public static class InstanceIdHeader
{
public static readonly String HeaderName = "InstanceId";
public static readonly String HeaderNamespace
= OrganizationalConstants.ServiceContractNamespace;
}
我们使用这个 static
类来放置标头,以及在服务器上检索标头。
void AddCustomHeaders(IClientChannel channel)
{
MessageHeader shareableInstanceContextHeader = MessageHeader.CreateHeader(
InstanceIdHeader.HeaderName,
InstanceIdHeader.HeaderNamespace,
instanceId.ToString());
var scope = new OperationContextScope(channel);
OperationContext.Current.OutgoingMessageHeaders.Add(shareableInstanceContextHeader);
}
因此,当我们创建服务通道时,我们添加自定义标头,如下面的摘录所示。
public TChannel GetChannel<TChannel>()
{
Type serviceType = typeof(TChannel);
object service;
channelsLock.EnterUpgradeableReadLock();
try
{
if (!channels.TryGetValue(serviceType, out service))
{ /* Value not in cache, therefore we create it. */
channelsLock.EnterWriteLock();
try
{
/* We don't cache the factory as it contains a list of channels
* that aren't removed if a fault occurs. */
var channelFactory = new ChannelFactory<TChannel>("*");
service = channelFactory.CreateChannel();
AddCustomHeaders((IClientChannel)service);
var communicationObject = (ICommunicationObject)service;
communicationObject.Faulted += OnChannelFaulted;
channels.Add(serviceType, service);
communicationObject.Open();
ConnectIfClientService(service, serviceType);
UnitySingleton.Container.RegisterInstance<TChannel>((TChannel)service);
}
finally
{
channelsLock.ExitWriteLock();
}
}
}
finally
{
channelsLock.ExitUpgradeableReadLock();
}
return (TChannel)service;
}
我们将通道缓存起来,直到它关闭或出现故障。但一旦我们创建了一个新通道,我们就添加了将在服务器上使用的标头。我们还支持双工通道,其工作方式基本相同。
在尝试在服务器端使用消息服务之前,我们必须与 ICommunicationService
通信以创建回调。这由 CommunicationModule
自动完成。事实上,Communication Module 会定期轮询服务器,以告知服务器它仍然处于活动状态。它还会检测网络连接(或缺乏连接),并相应地启用或禁用轮询。
/// <summary>
/// Notifies the server communication service that the client is still alive.
/// Occurs on a ThreadPool thread. <see cref="NotifyAlive"/>
/// </summary>
/// <param name="state">The unused state.</param>
void NotifyAliveAux(object state)
{
try
{
if (!NetworkInterface.GetIsNetworkAvailable())
{
return;
}
var channelManager = UnitySingleton.Container.Resolve<IChannelManager>();
var communicationService
= channelManager.GetDuplexChannel<ICommunicationService>(callback);
communicationService.NotifyAlive();
if (!connected)
{
connected = true;
connectionEvent.Publish(ConnectionState.Connected);
}
lastExceptionType = null;
}
catch (Exception ex)
{
Type exceptionType = ex.GetType();
if (exceptionType != lastExceptionType)
{
Log.Warn("Unable to connect to communication service.", ex);
}
lastExceptionType = exceptionType;
if (connected)
{
connected = false;
connectionEvent.Publish(ConnectionState.Disconnected);
}
}
finally
{
connecting = false;
}
}
顺便说一句,Communication Module 本身没有 UI。它的目的是与 CommunicationService
交互,并在各种服务器端事件发生时向客户端提供通知。其中一个 CompositeEvent
是 ConnectionEvent
,可以在上一个摘录中看到。
我计划扩展消息服务系统以支持文本输入、下拉列表选择,甚至可能自定义对话框。
要了解有关 Calcium 的更多信息,请访问 CodePlex 网站。