65.9K
CodeProject 正在变化。 阅读更多。
Home

位置无关的消息服务

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.64/5 (5投票s)

2009年8月29日

LGPL3

6分钟阅读

viewsIcon

14098

位置无关的消息服务

在开发应用程序时,显然在执行某些任务时保持一致性是明智的,从而避免违反 DRY 原则。一个例子是显示通用对话框。但请注意,如果您认为这篇博文仅仅是关于一个抽象的对话框系统,那么请三思。Calcium 确实提供了一个通用的对话框系统,但它也允许我们在任何 WCF 调用中从服务器向用户显示对话框!此外,它还允许我们在客户端和服务器上使用相同的 API。这意味着我们可以直接与用户交互,而无需在客户端解释 WCF 调用的结果。它有效地缩小了层级差距。

在这篇博文中,我们首先将讨论客户端消息服务,然后检查它如何从服务器端利用以提供位置无关性。

因此,显然,开发团队的每个成员都为简单的任务(例如向用户提问一个封闭式问题,如“是/否”问题框)创建自己的对话框是不明智的。如果我们决定在所有对话框中更改标题,那么当对话框分散在项目中的时候,这将变得相当困难。同样,如果我们希望将应用程序从 WPF 移植到 Silverlight,甚至移植到命令行界面(如 Powershell 或移动应用程序),能够为任何给定的场景替换实现将非常方便。显然,一个抽象的系统是必要的。

Calcium 开箱即用地提供了多种消息服务实现。有一个适用于 WPF 的客户端实现,另一个适用于命令行驱动应用程序的客户端实现,还有一个服务器端实现,它通过回调将消息发送回客户端,并利用客户端的 IMessageService 实现。

消息服务有各种重载,允许指定标题和消息。

让我们看一下 IMessageService 接口和客户端实现。

图示:IMessageService 允许我们以 UI 无关的方式与用户进行交互。

消息服务允许指定消息重要性级别。这允许用户设置一个阈值噪音级别。如果 MessageImportance 低于用户的偏好,则用户将不会被消息打扰。在 Calcium 的后续版本中,我们将看到一个首选项服务,用于指定首选级别。

图示:MessageServiceCommandLineMessageService 都重写了 MessageServiceBaseShowCustomDialog 方法,以适应它们各自的环境。

通过仅仅重写 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 原生枚举 MessageBoxButtonMessageBoxImageMessageBoxResult 之间进行转换。为什么要费这么大力气?乍一看,这似乎是一种反模式。实际上,原因很好: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 服务调用识别应用程序实例,我们将一个自定义标头放入我们创建的每个服务通道中,如下面的 ServiceManagerSingletonInstanceIdHeader 类的摘录所示。

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 交互,并在各种服务器端事件发生时向客户端提供通知。其中一个 CompositeEventConnectionEvent,可以在上一个摘录中看到。

我计划扩展消息服务系统以支持文本输入、下拉列表选择,甚至可能自定义对话框。

要了解有关 Calcium 的更多信息,请访问 CodePlex 网站

© . All rights reserved.