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

GeoPlaces:混合智能客户端,涉及 RESTful WCF/WPF 等

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (113投票s)

2009 年 4 月 1 日

CPOL

33分钟阅读

viewsIcon

307129

downloadIcon

2534

关于如何使用 RESTful WCF 和 WPF 的一个很好的示例。

目录

引言

我上一篇文章是 Sonic,在发布时,我称其为我迄今为止最好的文章。现在 Sonic 确实很棒,并且使用了许多不错的技术,但我不得不说,在我完成这篇文章后,我觉得自己更喜欢这一篇。无论如何,你们可以自行判断。我想我们会从说明这篇文章的实际功能/不实现的功能开始……那么,我们开始吧。

从某种意义上说,这篇文章可能最为“无用”(是的,你没看错,是无用),因为它不解决任何特定的领域问题,也不会让你在 1 小时内掌握,或者在你急需一个组件来神奇地弥补缺失代码时救你于水火。然而,从另一个层面来说,它应该能很好地作为你应该如何使用 X/Y 和 Z 技术的模板。

在这种情况下,X/Y 和 Z 技术将是 WCF(或者我喜欢的 Dub CF),以及新的 .NET 3.5 RESTful WCF 的可能性,还有我个人最喜欢的 WPF(Dub PF),它还利用 ADO.NET 实体框架进行持久化。

所以,本质上,这篇文章是关于 WCF/RESTful WCF/WPF 的。现在,为了向你展示如何做我认为在处理这些技术时很重要的事情,我需要一个能够使用这些技术来解决的问题。我知道,我知道,我已经说过这篇文章不解决任何问题;嗯,有一个假设性的领域问题是这篇文章要解决的。为此,我开发了附加的演示代码。那么,它到底做了什么?别卖关子了,Sacha,别再唠叨了,告诉我它做了什么。好吧,这很简单,我创建了以下内容:

  • 一个 RESTful WCF 服务,允许:
    • 现有用户登录,并与持久化的用户匹配
    • 新用户注册,并使用姓名和密码进行持久化
    • 存储与用户关联的新的地理兴趣点
    • 检索与用户关联的所有地理兴趣点
  • 一个 ADO.NET Framework 实体模型,允许将数据持久化到 SQL Server 2005(或 2008)。
  • 一个 WPF 客户端,允许所有服务调用正常工作,并以丰富的客户端方式显示结果。实际上,我必须说,我认为这个 WPF 客户端很酷,因为它使用托管的 Microsoft Virtual Earth 实例来显示用户的地理兴趣点,并在 VE 地球上显示,并允许完整的 VE 功能(但不包括分层)。WPF 客户端还拥有大量样式化/自定义控件,并使用一些 3D 技术来增加额外的趣味性,因此希望对于大多数 UI 开发者来说,都会有一些让他们感兴趣的地方。

虽然演示应用程序的领域非常简单,但它包含了足够的内容,让我能够演示在处理 WPF/WCF/RESTful WCF 时您需要了解的大部分内容,并且它还使用了 ADO.NET 实体框架,有些读者可能以前从未接触过。

本文包含许多代码片段,可能太多了,但我只是觉得它们对于更好地演示文本都非常必要。所以,如果你们对此感到不知所措,我感到抱歉。

查看演示

好了,在我们继续之前,让我先向你们展示一下它的样子。基本上,应用程序有三个步骤。用户除非完成登录或注册过程,否则无法进入下一步。 1.

步骤 1:登录或注册

如果登录,用户必须输入其预先保存的用户名和密码(注册时使用的详细信息),或者他们可以注册为新用户。您可以看到,左侧和右侧还有两个类似箭头的按钮,允许您前进/后退到之前的步骤。

注意:文章的代码包含一个 SQL 脚本,用于设置一个用户和一些兴趣点,以便您可以从一些虚拟数据开始。我将在 运行您的应用程序所需的操作 部分更详细地讨论这一点。

步骤 2:创建新地点/查看您保存的地点

用户登录后,STEP 导航按钮将被启用,这将允许用户跳到步骤 2。UI 本身将执行一个平滑的动画来进入步骤 2。它看起来不错,请亲自尝试一下。

当用户在步骤 2 时,他们可以输入有关地理兴趣点的新详细信息。为此,用户需要输入以下信息:

  • 地点名称
  • 地点描述
  • 地点纬度(以便在托管的 Virtual Earth 实例上显示)
  • 地点经度(以便在托管的 Virtual Earth 实例上显示)

这些详细信息的输入是通过一个托管在 3D 可翻转表面上的控件来完成的,这样用户就可以输入一个新地点,然后翻转 3D 表面,查看他们已经保存的所有地点(通过 ADO.NET 实体框架模型持久化到 SQL)。

这是用户在步骤 2 时默认看到的:

这是 3D 翻转过程中:

翻转后,这是所有地点(显示在 3D 网格的背面):

步骤 3:允许用户通过 Virtual Earth 查看已保存的地点

最后一步允许用户在托管的 Virtual Earth 实例上查看已保存的地点。显然,如果当前用户没有保存任何地点,则不会有任何项目显示。

假设有地点可以显示,用户可以选择一个,托管的 VE 实例将显示它并进行飞行缩放到用户选择的地点。托管的 VE 实例还可以通过以下方式从 WPF 客户端进行控制:

  • 使用提供的按钮放大/缩小
  • 使用提供的按钮或仅使用鼠标进行平移
  • 使用提供的按钮将模式更改为混合/仅路线或仅航空。

这是一个显示我最喜欢的地点之一“克莱斯勒大厦”在纽约市的例子,首先,用户点击已保存的地点:

然后我们自动缩放到它:

这就是它的样子。有些人可能不喜欢视觉风格,但写文章是一件很个人的事情,这就是我喜欢的风格,所以我按照我喜欢的方式来做,因为这是我的文章,很简单。

必备组件

正如我在本文开头提到的,本文使用了以下技术:

  • WCF
  • WPF
  • RESTful WCF
  • ADO.NET 实体框架 / SQL Server
  • Virtual Earth

因此,显然有一系列先决条件需要满足,如果您想在家/工作时运行本文的代码。这个列表如下:

  1. SQL Server 2005/2008
  2. .NET 3.5 SP1(因为我使用了一些 .NET 3.5 SP1 随附的最新 RESTful WCF 功能,以及用于托管 VE 实例的 DirectX 互操作的 D3DImage
  3. Visual Studio 2008
  4. Microsoft Visual Studio 2008 Service Pack 1
  5. Virtual Earth 3D (Beta)

运行您的应用程序所需的操作

为了使代码能够运行,您需要下载并安装所有上述先决条件。完成之后,您应该按照以下顺序进行操作:

1. 创建空的 SQL Server 数据库

在您自己的 SQL Server 安装中,创建一个名为“GeoPlaces”的新数据库。

2. 设置 SQL Server 数据库

文章的代码包含一个 SQL 脚本,可用于设置实际的 SQL Server 数据库架构。此 SQL 脚本名为“GenerateDBScript.sql”。此脚本必须在名为“GeoPlaces”的现有 SQL Server 数据库上运行。此脚本仅向“GeoPlaces”数据库添加两个表;这两个表将是“Users”和“Places”。

3. 设置 SQL 虚拟数据

文章的代码包含一个 SQL 脚本,可用于设置一些初始的虚拟 SQL 数据。此 SQL 脚本名为“CreateInitialDummyData.sql”。此 SQL 脚本仅添加一个名为“sacha”的用户,密码为“sacha”,以及我最喜欢的几个地点,因此如果您以“sacha”和密码“sacha”登录,托管的 VE 实例将有一些地点可以显示。

4. 更改 GeoPlacesServiceHost 项目中的 App.Config

文章的演示代码包含一个解决方案,其中有一个名为 GeoPlacesServiceHost 的项目;您现在应该修改 App.Config 以指向您自己的 SQL Server 安装。这些是您需要更改的行:

<!-- HOME-->
<connectionStrings>
    <add name="GeoPlacesEntities" 
     connectionString="metadata=res://GeoPlacesData/GeoPlacesModel.csdl|
                       res://GeoPlacesData/GeoPlacesModel.ssdl|
                       res://GeoPlacesData/GeoPlacesModel.msl;
                       provider=System.Data.SqlClient;
                       provider connection string="Data Source=YOUR SQL NAME HERE;
                       Initial Catalog=GeoPlaces;User ID=sa;Password=sa;
                       MultipleActiveResultSets=True"" 
     providerName="System.Data.EntityClient" />
</connectionStrings>

您需要更改的部分是 ADO.NET 实体框架连接字符串。这应该很简单,只需更改 DataSource 指向您的 SQL Server 实例即可。

5. 启动/安装服务宿主

为了运行任何种类的 WCF 服务(RESTful 或 SOAP),WCF 服务必须在某个地方被托管。有多种选择:

  • IIS
  • 自托管控制台应用程序
  • 在 Windows 服务中

文章的演示代码旨在支持自托管控制台应用程序,该应用程序将在运行时托管 RESTful WCF 服务,或者能够安装一个实际的 Windows 服务来托管 RESTful WCF 服务。

我无法告诉您选择哪种选项,这取决于您。相反,我将逐一介绍我支持的所有托管选项,您可以决定使用哪种。当然,如果您希望 WPF 客户端能够调用 RESTful WCF 服务,您必须使用并运行其中一个托管选项。

我开发了一个简单的类,可以同时支持将 WCF 服务作为自托管控制台应用程序在 Windows 服务中进行托管。基本思想是检查当前的生成模式(Debug|Release),如果是 Debug,我将在控制台应用程序中启动 RESTful WCF 托管,并设置无限超时。如果当前生成模式是 Release,我将尝试使用实际的 Windows 服务宿主。

执行此操作的骨架代码如下所示:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.ServiceProcess;

namespace GeoPlacesServiceHost
{
    static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// Runs as console app if in debug.
        /// </summary>
        static void Main()
        {
#if (!DEBUG)
            try
            {
                Console.WriteLine(String.Format(
                      "Initialising GeoPlacesDataService.GeoService in assembly " +
                      "{0} RELEASE windows service mode.",
                      Assembly.GetExecutingAssembly().FullName));

                ServiceBase[] ServicesToRun;
                ServicesToRun = new ServiceBase[] { new Service() };
                ServiceBase.Run(ServicesToRun);
            }
            catch (Exception ex)
            {
                Console.WriteLine(String.Format("Exception Occurred :", ex.Message));
            }
#else
            try
            {
                Console.WriteLine(String.Format(
                      "Initialising GeoPlacesDataService.GeoService " + 
                      "in assembly {0} DEBUG console mode.",
                      Assembly.GetExecutingAssembly().FullName));

                Console.WriteLine("Starting GeoPlacesDataService.GeoService");
                Service.StartService();
                Console.WriteLine("GeoPlacesDataService.GeoService Started");

                System.Threading.Thread.Sleep(System.Threading.Timeout.Infinite);
            }
            catch (Exception ex)
            {
                Console.WriteLine(String.Format("Exception Occurred :", ex.Message));
            }
#endif
        }
    }
}

如果您有兴趣,实际的 Windows 服务类如下所示:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.ServiceModel;
using System.ServiceModel.Description;
using System.ServiceModel.Web;
using System.ServiceProcess;
using System.Text;
using GeoPlacesDataService;

namespace GeoPlacesServiceHost
{
    /// <summary>
    /// Windows service to host the actual Restful 
    /// WCF KMLService
    /// </summary>
    public partial class Service : ServiceBase
    {
        #region Data
        private static ServiceHost GeoPlacesServiceHost;
        #endregion

        #region Ctor
        public Service()
        {
            InitializeComponent();
        }
        #endregion

        #region Overrides
        protected override void OnStart(string[] args)
        {
            Service.StartService();
        }

        protected override void OnStop()
        {
            try
            {
                Service.StopServiceHost(GeoPlacesServiceHost);
            }
            catch (Exception ex)
            {

                Console.WriteLine(String.Format(
                    "Exception while attempting to stop GeoService " +
                    "service type {0} the following exception was thrown {1}.",
                    this.GetType().FullName, ex.ToString()));
            }
        }
        #endregion

        #region Public Methods
        public static void StartService()
        {
            try
            {
                GeoPlacesServiceHost = new WebServiceHost(typeof(GeoService),
                    new Uri(ConfigurationManager.AppSettings[
                        "GeoServiceEndpointAddress"]));

                StartServiceHost(GeoPlacesServiceHost);
            }
            catch (TargetInvocationException tiEx)
            {
                Console.WriteLine(String.Format("Exception occurred", tiEx.Message));
            }
            catch (Exception ex)
            {
                Console.WriteLine(String.Format("Exception occurred", ex.Message));
            }
        }
        #endregion

        #region Private Methods
        private static void StartServiceHost(ServiceHost serviceHost)
        {

            Boolean openSucceeded = false;

            try
            {

                serviceHost = new WebServiceHost(typeof(GeoService),
                       new Uri(ConfigurationManager.AppSettings[
                           "GeoServiceEndpointAddress"]));

                serviceHost.Open();
                openSucceeded = true;
            }
            catch (Exception ex)
            {
                Console.WriteLine(String.Format(
                    "A failure occurred trying to open the " +
                    "GeoService ServiceHost, Error message : {0}",
                        ex.Message));
            }
            finally
            {
                if (!openSucceeded)
                {
                    serviceHost.Abort();
                    Console.WriteLine(String.Format("{0} Aborted.",
                        serviceHost.Description.Name));
                }
            }

            if (serviceHost.State == CommunicationState.Opened)
            {
                serviceHost.Faulted += ServiceHost_Faulted;
                Console.WriteLine("GeoService is running...");
            }
            else
            {
                Console.WriteLine("GeoService failed to open");
                Boolean closeSucceeded = false;
                try
                {
                    serviceHost.Close();
                    closeSucceeded = true;
                    Console.WriteLine(String.Format("{0} Closed.",
                        serviceHost.Description.Name));
                }
                catch (Exception ex)
                {
                    Console.WriteLine(String.Format(
                        "A failure occurred trying to close the " +
                        "GeoServicee ServiceHost, Error message : {0}",
                            ex.Message));
                }
                finally
                {
                    if (!closeSucceeded)
                    {
                        serviceHost.Abort();
                        Console.WriteLine(String.Format("{0} Aborted.",
                            serviceHost.Description.Name));
                    }
                }
            }
        }

        private static void StopServiceHost(ServiceHostBase serviceHost)
        {
            if (serviceHost.State != CommunicationState.Closed)
            {
                Boolean closeSucceeded = false;
                try
                {
                    serviceHost.Close();
                    closeSucceeded = true;
                    Console.WriteLine(String.Format("{0} Closed.",
                        serviceHost.Description.Name));
                }
                catch (Exception ex)
                {
                    Console.WriteLine(String.Format(
                        "A failure occurred trying to close the " +
                        "GeoService ServiceHost, Error message : {0}",
                            ex.Message));
                }
                finally
                {
                    if (!closeSucceeded)
                    {
                        serviceHost.Abort();
                        Console.WriteLine(String.Format("{0} Aborted.",
                            serviceHost.Description.Name));
                    }
                }
            }
        }

        private static void RestartServiceHost(ServiceHost serviceHost)
        {
            StopServiceHost(serviceHost);
            StartServiceHost(serviceHost);
        }

        private static void LogServiceHostInfo(ServiceHostBase serviceHost)
        {
            var strBuilder = new StringBuilder();
            strBuilder.AppendFormat("'{0}' Starting", serviceHost.Description.Name);
            strBuilder.Append(Environment.NewLine);

            // Behaviors
            var annotation = 
                serviceHost.Description.Behaviors.Find<ServiceBehaviorAttribute>();

            strBuilder.AppendFormat("Concurrency Mode = {0}", 
                annotation.ConcurrencyMode);
            strBuilder.Append(Environment.NewLine);
            strBuilder.AppendFormat("InstanceContext Mode = {0}", 
                annotation.InstanceContextMode);
            strBuilder.Append(Environment.NewLine);

            // Endpoints
            strBuilder.Append("The following endpoints are exposed:");
            strBuilder.Append(Environment.NewLine);
            foreach (ServiceEndpoint endPoint in serviceHost.Description.Endpoints)
            {
                strBuilder.AppendFormat("{0} at {1} with {2} binding; "
                       , endPoint.Contract.ContractType.Name
                       , endPoint.Address
                       , endPoint.Binding.Name);
                strBuilder.Append(Environment.NewLine);
            }

            // Metadata
            var metabehaviour = 
                serviceHost.Description.Behaviors.Find<ServiceMetadataBehavior>();

            if (metabehaviour != null)
            {
                if (metabehaviour.HttpGetEnabled)
                {
                    if (metabehaviour.HttpsGetUrl != null)
                    {
                        strBuilder.AppendFormat("Metadata enabled at {0}", 
                            serviceHost.BaseAddresses[0]);
                    }
                    else
                    {
                        strBuilder.AppendFormat("Metadata enabled at {0}", 
                            metabehaviour.HttpGetUrl);
                    }
                }
                if (metabehaviour.HttpsGetEnabled)
                    strBuilder.AppendFormat(" and {0}.", metabehaviour.HttpsGetUrl);
                if (metabehaviour.ExternalMetadataLocation != null)
                    strBuilder.AppendFormat(" Metadata can be found externally at {0}", 
                        metabehaviour.ExternalMetadataLocation);
            }

            Console.WriteLine(strBuilder.ToString());
        }

        private static void ServiceHost_Faulted(Object sender, EventArgs e)
        {
            var serviceHost = sender as ServiceHost;
            Console.Write(String.Format("{0} Faulted. Attempting Restart.",
                serviceHost.Description.Name));
            RestartServiceHost(serviceHost);
        }
        #endregion
    }
}

还有一个 Windows 服务的安装程序,但那都是非常标准的,所以我就不在此赘述了。

总之,我只是想解释一下托管的方法。现在,让我们讨论一下如何使用这些不同的托管方法。

自托管控制台应用程序

这可能是托管 WCF 服务的最简单方法,因为它只是一个简单的类,带有一个主方法和一些托管服务的代码,仅此而已。要使用此方法,您只需导航到 GeoPlacesServiceHost\bin\Debug\ 文件夹并运行 GeoPlacesServiceHost.exe 控制台应用程序。

然后您应该看到宿主已启动,如下所示:

Windows 服务托管

正如我所说,我创建了一个类,允许在 Debug 模式下进行控制台托管,在 Release 模式下进行 Windows 服务托管。为了在 Windows 服务中托管 WCF 服务,我们需要执行以下步骤:

安装服务

从命令行运行:installutil.exe "XXXXXXXXX\GeoPlacesServiceHost\bin\Release\GeoPlacesServiceHost.exe",其中 XXXXXXXXX 是您下载的 GeoPlaces 代码的路径。

这将显示一个登录屏幕,您需要填写该屏幕才能运行服务。

这将在可用 Windows 服务列表中添加一个新的 Windows 服务。安装后,您需要启动 Windows 服务。转到可用 Windows 服务列表并启动 GeoPlaces 服务。

我个人更喜欢在 Visual Studio 中开发代码时使用控制台应用程序方法,而对于实际完成的可部署代码则使用 Windows 服务方法。我认为 Windows 服务托管很酷,因为实际的 Windows 服务可以管理托管的 RESTful WCF 服务的启动/停止。这很酷,因为我不再需要担心了;基本上,Windows 服务管理着托管的 RESTful WCF 服务的生命周期。

RESTful WCF 服务

WCF 已经存在一段时间了,有很多关于使用它的文章,所以我不会涵盖基础知识(如果您愿意,可以阅读我的一篇旧 WCF 文章来学习关于基于 SOAP 的 WCF 的基础知识);相反,我将讨论如何处理新的 RESTful WCF 功能。

对于那些甚至不知道 REST 是什么的人来说,REST 代表 Representational State Transfer(REST)。该术语通常更宽松地用于描述任何通过 HTTP 传输领域特定数据的简单接口,而无需额外的消息层,例如 SOAP 或通过 HTTP Cookie 进行的会话跟踪。

当我们处理 RESTful WCF 时,我们实际上是在讨论将类型公开为 JSON 或 XML,并位于特定 URI 下。

正如你们中的一些人可能知道,标准的基于 SOAP 的 WCF 通过端点公开,RESTful WCF 也不例外;唯一的区别是,除了用于与 RESTful WCF 服务通信的代理类之外,用户还可以使用标准浏览器。

这是通过使用标准的 HTTP 动词实现的:

  • GET:检索资源
  • PUT:更新现有资源
  • POST:创建新资源
  • DELETE:删除资源

那么这一切是如何工作的呢?嗯,其实很简单。.NET 3.5 的一部分允许我们将 WCF 服务的 OperationContract 用额外的 RESTful 属性进行装饰。让我们来研究一下其中的一些。

GET

在 GeoPlaces 数据库中(如果您已用示例数据填充),将有一些地点与特定用户关联(对我来说,该用户是 10,对您来说可能不同),所以我们在 SQL Server 中有一个如下所示的表:

我们还有一个如下所示的 RESTful WCF 服务方法:

[OperationContract]
[WebGet(UriTemplate = "/placesList/{userId}",
ResponseFormat = WebMessageFormat.Xml)]
List<Places> GetAllPlacesForUser(String userId);

注意 WebGetAttribute 的使用,其中指定了 UriTemplate,这允许用户在浏览器中输入一个 URI,该 URI 将与 RESTful WCF 服务中的所有方法进行模式匹配,以查看是否存在匹配项。如果存在匹配项,则会调用该方法,并且返回值将被序列化为用户指定的格式,在本例中为 XML。

实际的方法实现如下所示:

/// <summary>
/// Gets all places for a particular user
/// </summary>
public List<Places> GetAllPlacesForUser(String userId)
{
    Int32 id = -1;
    if (Int32.TryParse(userId, out id))
    {
        try
        {
            GeoPlacesEntities model = new GeoPlacesEntities();
            return model.Places.Where((pl) => pl.Users.ID == id).ToList();
        }
        catch (Exception ex)
        {
            //WebProtocolException is part of WCF REST Starter Kit Preview 2
            throw new WebProtocolException(HttpStatusCode.BadRequest,
                String.Format("Couldn't find places for user id {0}", userId), null);

        }
    }
    else
        return null;
}

那么,用户应该如何调用 RESTful 服务方法使用 GET 呢?嗯,这仍然取决于 WCF 服务在某个地方被托管。在附加的演示代码中,WCF 服务可以托管在控制台应用程序中,也可以托管在 Windows 服务中;在这两种情况下,都有一个 App.Config 设置指示 RESTful 服务将在以下地址可用:“https://:8085/GeoPlacesDataService”。因此,如果 RESTful WCF 服务已托管并正在运行,用户就可以输入以下内容,这与我们上面看到的 WebGetAttribute UriTemplate 匹配:

https://:8085/GeoPlacesDataService + /placesList/{userId},并在浏览器中进行实际替换以获取资源。例如:

https://:8085/GeoPlacesDataService/placesList/10,我们期望它返回 ID 为 10 的用户的地点资源列表。让我们在浏览器中试试。

注意:方法参数必须UriTemplate 中的替换部分匹配,并且所有参数都必须是 String 类型,因为通过浏览器只能处理这些类型。

果然,它奏效了。这都要归功于模式匹配和 WebGetAttribute,它指定了一个 UriTemplate,允许模式匹配工作并找到要调用的正确方法。

您可以看到这是 XML,您可能会想,这个 XAML 来自哪里?嗯,演示代码使用 ADO.NET 实体框架将数据持久化到/从 SQL Server,所以这是演示代码的 ADO.NET 实体框架模型中 Places 对象默认的序列化。为了完全理解这里发生的情况,我将展示 Places 类是什么样子的:

/// <summary>
/// There are no comments for GeoPlacesModel.Places in the schema.
/// </summary>
/// <KeyProperties>
/// ID
/// </KeyProperties>
[global::System.Data.Objects.DataClasses.EdmEntityTypeAttribute(
    NamespaceName="GeoPlacesModel", Name="Places")]
[global::System.Runtime.Serialization.DataContractAttribute(IsReference=true)]
[global::System.Serializable()]
public partial class Places : global::System.Data.Objects.DataClasses.EntityObject
{
    /// <summary>
    /// Create a new Places object.
    /// </summary>
    /// <param name="ID">Initial value of ID.</param>
    /// <param name="name">Initial value of Name.</param>
    /// <param name="description">Initial value of Description.</param>
    /// <param name="longitude">Initial value of Longitude.</param>
    /// <param name="latitude">Initial value of Latitude.</param>
    public static Places CreatePlaces(int ID, string name, 
        string description, double longitude, double latitude)
    {
        Places places = new Places();
        places.ID = ID;
        places.Name = name;
        places.Description = description;
        places.Longitude = longitude;
        places.Latitude = latitude;
        return places;
    }
    /// <summary>
    /// There are no comments for Property ID in the schema.
    /// </summary>
    [global::System.Data.Objects.DataClasses.EdmScalarPropertyAttribute(
        EntityKeyProperty=true, IsNullable=false)]
    [global::System.Runtime.Serialization.DataMemberAttribute()]
    public int ID
    {
        get
        {
            return this._ID;
        }
        set
        {
            this.OnIDChanging(value);
            this.ReportPropertyChanging("ID");
            this._ID = global::System.Data.Objects.DataClasses.
                StructuralObject.SetValidValue(value);
            this.ReportPropertyChanged("ID");
            this.OnIDChanged();
        }
    }
    private int _ID;
    partial void OnIDChanging(int value);
    partial void OnIDChanged();
    /// <summary>
    /// There are no comments for Property Name in the schema.
    /// </summary>
    [global::System.Data.Objects.DataClasses.EdmScalarPropertyAttribute(IsNullable=false)]
    [global::System.Runtime.Serialization.DataMemberAttribute()]
    public string Name
    {
        get
        {
            return this._Name;
        }
        set
        {
            this.OnNameChanging(value);
            this.ReportPropertyChanging("Name");
            this._Name = global::System.Data.Objects.DataClasses.
                StructuralObject.SetValidValue(value, false);
            this.ReportPropertyChanged("Name");
            this.OnNameChanged();
        }
    }
    private string _Name;
    partial void OnNameChanging(string value);
    partial void OnNameChanged();
    /// <summary>
    /// There are no comments for Property Description in the schema.
    /// </summary>
    [global::System.Data.Objects.DataClasses.EdmScalarPropertyAttribute(IsNullable=false)]
    [global::System.Runtime.Serialization.DataMemberAttribute()]
    public string Description
    {
        get
        {
            return this._Description;
        }
        set
        {
            this.OnDescriptionChanging(value);
            this.ReportPropertyChanging("Description");
            this._Description = global::System.Data.Objects.DataClasses.
                StructuralObject.SetValidValue(value, false);
            this.ReportPropertyChanged("Description");

            this.OnDescriptionChanged();
        }
    }
    private string _Description;
    partial void OnDescriptionChanging(string value);
    partial void OnDescriptionChanged();
    /// <summary>
    /// There are no comments for Property Longitude in the schema.
    /// </summary>
    [global::System.Data.Objects.DataClasses.
        EdmScalarPropertyAttribute(IsNullable=false)]
    [global::System.Runtime.Serialization.DataMemberAttribute()]
    public double Longitude
    {
        get
        {
            return this._Longitude;
        }
        set
        {
            this.OnLongitudeChanging(value);
            this.ReportPropertyChanging("Longitude");
            this._Longitude = global::System.Data.Objects.DataClasses.
                StructuralObject.SetValidValue(value);
            this.ReportPropertyChanged("Longitude");
            this.OnLongitudeChanged();
        }
    }
    private double _Longitude;
    partial void OnLongitudeChanging(double value);
    partial void OnLongitudeChanged();
    /// <summary>
    /// There are no comments for Property Latitude in the schema.
    /// </summary>
    [global::System.Data.Objects.DataClasses.EdmScalarPropertyAttribute(IsNullable=false)]
    [global::System.Runtime.Serialization.DataMemberAttribute()]
    public double Latitude
    {
        get
        {
            return this._Latitude;
        }
        set
        {
            this.OnLatitudeChanging(value);
            this.ReportPropertyChanging("Latitude");
            this._Latitude = global::System.Data.Objects.DataClasses.
                StructuralObject.SetValidValue(value);
            this.ReportPropertyChanged("Latitude");
            this.OnLatitudeChanged();
        }
    }
    private double _Latitude;
    partial void OnLatitudeChanging(double value);
    partial void OnLatitudeChanged();

您可以看到它实现了 Serializable,并且还具有 DataContractAttribute,它将使用 DataContractSerializer 进行序列化。您可以看到,它只是被序列化并作为 XML 发送(虽然不是非常漂亮的 XML,但它确实可以正常工作)。

如果您想更精细地控制资源的序列化方式,您可以编写自己的序列化,使用返回类型 Message。我在我的博客上详细讨论了处理 RESTful WCF 时的序列化选项;您可以从这篇文章中阅读您的选项:http://sachabarber.net/?p=475

好的,这就是基本思路。我们有一些新的 RESTful 属性,我们可以通过 HTTP URI 公开服务方法,并且我们可以对某些 URI 进行模式匹配,甚至可以使用 URI 的一部分作为方法的参数。

现在,我非常赞成在整个服务仅包含 GET 请求时使用浏览器,因为根本不需要 DataContract 客户端代理类。浏览器可以通过使用特定的 URI 来获取它想要的资源。然而,正如我们都知道的,事情并非总是那么简单,大多数(如果不是全部)应用程序都需要更新/删除和修改数据;为此,您几乎总是需要使用除 GET 请求之外的其他 HTTP 动词。

POST / PUT / DELETE

这些 HTTP 动词很有可能需要发送整个对象,这些对象要么是 XML/JSON 序列化,并且需要被反序列化并转换为 CLR 类型。现在,我喜欢编码,但我不是虐待狂,我总是会选择“为合适的工作选择合适的工具”;为此,我建议使用代理,我将在下面的 WPF 客户端 部分进行详细讨论。

目前,您只需要知道有一个 WebInvokeAttribute,可用于 POST/PUT 和 DELETE HTTP 动词。

这是一个例子。

[OperationContract]
[WebInvoke(Method = "POST", UriTemplate = "User/Add/",
    ResponseFormat = WebMessageFormat.Xml)]
Users AddUser(Users newUser);

WebInvokeAttribute必须同时指定一个 Method 类型。可能的值是 POST/PUT 和 DELETE;这是必需的,因为所有三个 HTTP 动词都可能以相同的 URI 模板结束,需要有一种方法来区分它们。

WebProtocolExceptions

那些使用过 WCF 的人可能遇到过 SOAP faults,在普通基于 SOAP 的 WCF 服务中,它们是通过 FaultException<T> 实现的。FaultException<T> 允许我们将 SOAP 故障流回 WCF 服务客户端应用程序。现在,由于我们使用的是 HTTP 和 RESTful 服务,我们不能再使用 FaultException<T>,因为我们需要一个 HTTP 代表性的故障消息。幸运的是,Microsoft 已经考虑到了这一点,并发布了一个 RESTful WCF 入门工具包,其中包含一些有用的类。其中之一是 WebProtocolException 类,它允许 WebProtocolExceptions 被视为与普通 WebProtocolExceptions 一样。然后 RESTful WCF 服务的使用者可以使用它们。下面是一个实际的 RESTful WCF 服务引发 WebProtocolException 的示例:

/// <summary>
/// Gets all places for a particular user
/// </summary>
public List<Places> GetAllPlacesForUser(String userId)
{
    Int32 id = -1;
    if (Int32.TryParse(userId, out id))
    {
        try
        {
            GeoPlacesEntities model = new GeoPlacesEntities();
            return model.Places.Where((pl) => pl.Users.ID == id).ToList();
        }
        catch (Exception ex)
        {
            //WebProtocolException is part of WCF REST Starter Kit Preview 2
            throw new WebProtocolException(HttpStatusCode.BadRequest,
                String.Format("Couldn't find places for user id {0}", userId), null);

        }
    }
    else
        return null;
}

请注意,在使用基于 SOAP 的 WCF 时,使用 FaultException<T> 意味着我们必须使用 FaultContractAttribute(s) 来标记 ServiceContract;使用 WebProtocolException 类则不需要这样做。

ETag(s)

维基百科关于 ETags 的说法是:

“ETag(实体标签)是符合 HTTP/1.1 的 Web 服务器返回的 HTTP 响应头,用于确定给定 URL 内容的更改。当新的 HTTP 响应包含与旧 HTTP 响应相同的 ETag 时,内容被认为相同,无需进一步下载。该头对于执行缓存的中间设备以及缓存结果的客户端 Web 浏览器很有用。”

然而,Jon Flanders,一本关于 REST 的优秀书籍的作者,我认为他的说法更好。他说:

“ETag 是每个资源独有的、不透明的、唯一的。ETag 通常是由服务器在响应资源 GET 请求时生成的哈希值,该哈希值基于资源本身的一些信息。当用户代理再次请求同一资源时,ETag 的值会出现在 If-None-Match 标头中。

当服务器收到请求时,它必须重新生成资源的 ETag,如果当前 ETag 与 If-No-Match 标头的值匹配,则表示资源未更改。”

- Jon Flanders,RESTful .NET,O'Reilly。

现在,这对 RESTful WCF 服务开发者来说意味着什么呢?当我们收到对资源的请求时,我们应该成为一个好的开发者,创建一个 ETag,以便在下一次请求时进行检查。

考虑演示代码中的 GET 方法:

[OperationContract]
[WebGet(UriTemplate = "/users/{userId}",
    ResponseFormat = WebMessageFormat.Xml)]
Users GetUser(String userId);

其实际实现如下所示:

/// <summary>
/// Gets a user based on its Id
/// </summary>
public Users GetUser(String userId)
{

    Int32 id = -1;
    if (Int32.TryParse(userId, out id))
    {
        try
        {
            Users u = FindUser(id);
#if HTTP
            string etag = GenerateETag(u.ID + u.Name + u.Password);

            if (CheckETag(etag))
                return null;

            if (u == null)
            {
                OutgoingWebResponseContext ctx =
                    WebOperationContext.Current.OutgoingResponse;
                ctx.SetStatusAsNotFound();
                ctx.SuppressEntityBody = true;
            }

            SetETag(etag);
#endif
            return u;

        }
        catch (Exception ex)
        {
            //WebProtocolException is part of WCF REST Starter Kit Preview 2
            throw new WebProtocolException(HttpStatusCode.BadRequest,
                String.Format("Couldn't find user with id {0}", userId), null);
        }
    }
    else
        return null;
}

您会注意到此代码使用三个 ETag 辅助方法(如果定义了 #HTTP),这些方法列在下面,用于管理 ETag 的检查和新 ETag 的创建。这允许资源的缓存。

/// <summary>
/// Sets a ETag (caching for the object) on the current
/// OutgoingResponse context
/// </summary>
private void SetETag(string etag)
{
    OutgoingWebResponseContext ctx =
        WebOperationContext.Current.OutgoingResponse;
    ctx.ETag = etag;
}

/// <summary>
/// Creates a ETag (caching for the object) 
/// </summary>
private  string GenerateETag(String valueToHash)
{
    byte[] bytes = Encoding.UTF8.GetBytes(valueToHash);
    byte[] hash = MD5.Create().ComputeHash(bytes);
    string etag = Convert.ToBase64String(hash);
    return etag;
}

/// <summary>
/// Examines the incoming request context to see if there
/// is a cached ETag for the object requested
/// </summary>
private bool CheckETag(string currentETag)
{
    IncomingWebRequestContext ctx =
        WebOperationContext.Current.IncomingRequest;
    string incomingEtag =
        ctx.Headers[HttpRequestHeader.IfNoneMatch];
    if (incomingEtag != null)
    {
        if (currentETag == incomingEtag)
        {
            return true;
        }
    }
    return false;
}

做得好:为新资源提供新 URI

由于 RESTful WCF 服务的使用者可能会添加新资源,他们应该能够通过 URI 访问这些添加的资源,所以当添加新资源时,您(开发者)为其创建一个新 URI 是很礼貌的。这在我们允许添加新用户时可以看到。

[OperationContract]
[WebInvoke(Method = "POST", UriTemplate = "User/Add/",
    ResponseFormat = WebMessageFormat.Xml)]
Users AddUser(Users newUser);

用户的资源可以通过以下方式获得:

[OperationContract]
[WebGet(UriTemplate = "/users/{userId}",
    ResponseFormat = WebMessageFormat.Xml)]
Users GetUser(String userId);

所以,如果我们检查 AddUser() 方法的实现,我们可以看到,当一个新用户的资源被创建时,我们也很礼貌地为新用户资源创建了一个新的 URI。这将通过当前 Web 上下文返回,然后用户就可以使用新 URI 来 GET 新添加的资源。

/// <summary>
/// Adds a user to the System
/// </summary>
public Users AddUser(Users newUser)
{
    try
    {
        GeoPlacesEntities model = new GeoPlacesEntities();
        model.AddToUsers(newUser);
        model.SaveChanges();

        Users userFromDb = model.Users.Where((u) =>
           u.Name.ToLower().Equals(newUser.Name) &&
           u.Password.ToLower().Equals(newUser.Password)
           ).First();

#if HTTP
        OutgoingWebResponseContext ctx = 
            WebOperationContext.Current.OutgoingResponse;
        ctx.SetStatusAsCreated(CreateNewUserUri(userFromDb));
#endif

        return userFromDb;
    }
    catch(Exception ex)
    {
        //WebProtocolException is part of WCF REST Starter Kit Preview 2
        throw new WebProtocolException(HttpStatusCode.BadRequest,
            "Couldn't add new user", null);
    }
}
        
/// <summary>
/// Create a new User Uri for the Resource
/// </summary>
private Uri CreateNewUserUri(Users u)
{
    UriTemplate ut = new UriTemplate("/users/{user_id}");
    Uri baseUri = WebOperationContext.Current.
        IncomingRequest.UriTemplateMatch.BaseUri;
    Uri ret = ut.BindByPosition(baseUri, u.ID.ToString());
    return ret;
}

WCF 可扩展性

我最后想谈谈 WCF 的可扩展性。基本上,如果您能想到任何想要扩展 WCF 的地方,都会有一个扩展点。例如,我希望所有 HTTP Content-Type 标头值都能自动设置,而无需我为每个请求/响应编写代码。

事实证明,您可以通过扩展 IEndpointBehavior 来实现这一点,如下所示:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel.Description;
using System.ServiceModel.Channels;
using System.ServiceModel.Dispatcher;

namespace GeoPlacesDataService
{
    /// <summary>
    /// Taken directly from the excellent RESTful .NET by Jon Flanders
    /// 
    /// It simply alters the endpoint behaviour by automatically setting the
    /// Content Type, by the use of the ContentTypeMessageInspector class
    /// </summary>
    public class ContentTypeBehaviour : IEndpointBehavior
    {

        public string ContentType { get; set; }

        #region IEndpointBehavior Members

        public void AddBindingParameters(ServiceEndpoint endpoint, 
            BindingParameterCollection bindingParameters)
        {
            //do nothing
        }

        public void ApplyClientBehavior(ServiceEndpoint endpoint, 
            ClientRuntime clientRuntime)
        {
            //work out what the correct ContentType should be
            ContentTypeMessageInspector mi = new 
                ContentTypeMessageInspector { ContentType = this.ContentType };
            clientRuntime.MessageInspectors.Add(mi);
        }

        public void ApplyDispatchBehavior(ServiceEndpoint endpoint, 
            EndpointDispatcher endpointDispatcher)
        {
            //do nothing
        }

        public void Validate(ServiceEndpoint endpoint)
        {
            //do nothing
        }

        #endregion
    }
}

这个类使用了名为 ContentTypeMessageInspector 的辅助类,如下所示:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel.Dispatcher;
using System.ServiceModel.Channels;
using System.ServiceModel;

namespace GeoPlacesDataService
{
    /// <summary>
    /// Taken directly from the excellent RESTful .NET by Jon Flanders
    /// 
    /// Automatically sets the Content Type, by extending WCF namely 
    /// setting the HttpRequestMessageProperty headers
    /// </summary>
    public class ContentTypeMessageInspector : IClientMessageInspector
    {
        public string ContentType { get; set; }

        #region IClientMessageInspector Members

        public void AfterReceiveReply(ref Message reply, object correlationState)
        {
            //do nothing
        }

        /// <summary>
        /// Apply the Content-Type header to the HttpRequestMessageProperty
        /// </summary>
        public object BeforeSendRequest(ref Message request, 
            IClientChannel channel)
        {
            HttpRequestMessageProperty prop = 
                request.Properties[HttpRequestMessageProperty.Name]
                    as HttpRequestMessageProperty;
            
            if (prop != null && (prop.Method == "POST" || 
                prop.Method == "PUT"))
            {
                prop.Headers["Content-Type"] = this.ContentType;
            }
            return null;

        }

        #endregion
    }
}

我不声称这两类代码是我自己写的;这些都是直接摘自 Jon Flanders 出色的 RESTful .NET 一书。Jon 很懂行,这本书很棒,强烈推荐。

WPF 客户端

有一个 WPF 客户端应用程序,它利用正在运行的 RESTful WCF 服务,该服务托管在 Windows 服务中(或者如果您只使用独立的 EXE,则自托管在控制台应用程序中)。本节将概述 WPF 客户端的最重要部分。

MVVM:模型-视图-视图模型模式

如果您使用 WPF,您应该使用 MVVM 模式,它允许视图被抽象到 ViewModel。其思想是 ViewModel 可以在隔离环境中进行测试,而 View 代码只包含实际的演示,即 XAML。通常,View 将绑定到关联 ViewModel 的属性,其中 ViewModel 将被设置为 View 的 DataContext

ViewModel 将公开 DependencyProperty 作为可绑定属性,或者公开 CLR 属性,其中 INotifyPropertyChanged 将用于确保更改通知(允许绑定更新到最新值)。

附加的演示代码包含多个 View 和 ViewModel,它们共同执行 WPF 客户端的各种功能,但如果您检查 View 的代码隐藏,您会发现代码很少。ViewModel 负责所有工作。以下是 View/ViewModel 列表:

View 名称 ViewModel 描述
LoginControl LoginViewModel 允许用户登录或注册
MainWindow MainWindowViewModel 主窗口
PlaceControl PlacesViewModel 表示用户的地点

让我们以登录 View/ViewModel 为例,我将在后续章节的大部分讨论中使用它:

这是 ViewModel 代码(注意ViewModelBase 实现 INotifyPropertyChanged 接口):

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Windows.Input;
using GeoPlacesModel;

namespace GeoPlaces
{
    /// <summary>
    /// Simple ViewModel for the LoginControl
    /// </summary>
    public class LoginViewModel : ViewModelBase
    {
        #region Data
        private String userName = String.Empty;
        private String password = String.Empty;
        private Boolean isAuthenticatedUser = false;
        private Boolean isBusy = false;
        private ICommand loginCommand = null;
        private ICommand registerCommand = null;
        private IView view = null;
        #endregion

        #region Ctor
        public LoginViewModel(IView view)
        {
            this.view = view;

            //wire up loginCommand
            loginCommand = new SimpleCommand
            {
                CanExecuteDelegate = x => !IsBusy,
                ExecuteDelegate = x => Login()
            };

            //wire up registerCommand
            registerCommand = new SimpleCommand
            {
                CanExecuteDelegate = x => !IsBusy,
                ExecuteDelegate = x => Register()
            };
        }
        #endregion

        #region Public Properties
        public ICommand RegisterCommand
        {
            get { return registerCommand; }
        }

        public ICommand LoginCommand
        {
            get { return loginCommand; }
        }

        public Boolean IsBusy
        {
            get { return isBusy; }
            set
            {
                isBusy = value;
                NotifyChanged("IsBusy");
            }
        }

        public String UserName
        {
            get { return userName; }
            set
            {
                userName = value;
                isAuthenticatedUser = false;
                NotifyChanged("IsAuthenticatedUser");
                NotifyChanged("UserName");
            }
        }

        public String Password
        {
            get { return password; }
            set
            {
                password = value;
                isAuthenticatedUser = false;
                NotifyChanged("IsAuthenticatedUser");
                NotifyChanged("Password");
            }
        }

        public Boolean IsAuthenticatedUser
        {
            get { return isAuthenticatedUser; }
            set
            {
                isAuthenticatedUser = value;
                NotifyChanged("IsAuthenticatedUser");
            }
        }
        #endregion

        #region Private Methods
        private void Login()
        {
            isBusy = true;

            App.Current.Properties.Remove("CurrentUser");


            Users dbReadUser = ServiceCalls.LoginUser(userName, password);
            if (dbReadUser != null)
            {
                App.Current.Properties.Add("CurrentUser", dbReadUser);
                Mediator.Instance.NotifyColleagues(
                    ViewModelMessages.IsAuthenticatedUser, true);
                view.ShowMessage(String.Format(
                    "Sucessfully logged in user {0}, " + 
                    "please proceed to view/add your places",
                    userName));
            }
            else
            {
                App.Current.Properties.Add("CurrentUser", null);
                Mediator.Instance.NotifyColleagues(
                    ViewModelMessages.IsAuthenticatedUser, false);
                view.ShowMessage(String.Format(
                    "Could not log in user {0}, please try again",
                    userName));
            }

            isBusy = false;
        }


        private void Register()
        {
            isBusy = true;

            App.Current.Properties.Remove("CurrentUser");

            Users dbReadUser = ServiceCalls.RegisterUser(userName, password);
            if (dbReadUser != null)
            {
                App.Current.Properties.Add("CurrentUser", dbReadUser);
                Mediator.Instance.NotifyColleagues(
                    ViewModelMessages.IsAuthenticatedUser, true);
                view.ShowMessage(String.Format(
                    "Sucessfully added user {0}, please " + 
                    "proceed to view/add your places",
                    userName));
            }
            else
            {
                App.Current.Properties.Add("CurrentUser", null);
                Mediator.Instance.NotifyColleagues(
                    ViewModelMessages.IsAuthenticatedUser, false);
                view.ShowMessage(String.Format(
                    "Could not add user {0}, please try again",
                        userName));
            }

            isBusy = false;
        }
        #endregion
    }
}

这是 LoginControl.xaml

<StackPanel Orientation="Vertical">
    <Label FontSize="18" Content="Login or Register"/>
    
    <StackPanel Orientation="Horizontal">

        <Label FontSize="14" 
           Content="UserName" Width="100"/>
        <TextBox Text="{Binding Path=UserName, Mode=TwoWay, 
            UpdateSourceTrigger=LostFocus}"/>
    
    </StackPanel>


    <StackPanel Orientation="Horizontal">

        <Label FontSize="14" 
            Content="Password" Width="100"/>
        <TextBox Text="{Binding Path=Password, Mode=TwoWay, 
            UpdateSourceTrigger=LostFocus}"/>

    </StackPanel>


    <StackPanel Orientation="Horizontal">
        <Button x:Name="btnLogin" Content="Login"
                Height="30" Margin="10" 
                Style="{StaticResource GelButton}"
                ToolTip="Login Using Your Previous Details"                    
                Command="{Binding Path=LoginCommand}"/>
        <Button x:Name="btnRegister" Content="Register"
                Height="30" Margin="10" 
                ToolTip="Register As A New User"                    
                Style="{StaticResource GelButton}"
                Command="{Binding Path=RegisterCommand}"/>
    </StackPanel>
</StackPanel>

ViewModel 在代码隐藏中被设置为 DataContext,如下所示。请注意代码隐藏是多么“愚蠢”。ViewModel 完成了大部分工作。View 非常被动,只是通过 ViewModel 的绑定更新来响应更改。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace GeoPlaces
{
    /// <summary>
    /// A simple Login control that allows existing
    /// users to login, or new users to register
    /// </summary>
    public partial class LoginControl : UserControl, IView
    {
        #region Data
        private LoginViewModel loginViewModel = null;
        #endregion

        #region Ctor
        public LoginControl()
        {
            InitializeComponent();
            this.Loaded += new RoutedEventHandler(LoginControl_Loaded);
            loginViewModel = new LoginViewModel(this);
        }
        #endregion

        #region Private Methods
        private void LoginControl_Loaded(object sender, RoutedEventArgs e)
        {
            this.DataContext = loginViewModel;
        }
        #endregion

        #region IView Members

        public void ShowMessage(string message)
        {
            MessageBox.Show(message,"information",
                MessageBoxButton.OK,MessageBoxImage.Information);
        }

        #endregion

    }
}

命令:Commands

为了全面支持 MVVM 模式,您需要考虑某种命令基础结构,以便 View 可以绑定到 ICommand 实例,通常公开为 ViewModel 上的属性,并且最好允许在 ViewModel 中执行命令的代码。虽然使用标准的 WPF RoutedCommands 可以做到这一点,但有点繁琐,因为它要求开发人员在 View 的 XAML 或代码隐藏中创建命令绑定并连接委托等,相信我,这很繁琐。

最近,一些人,包括 Microsoft Composite WPF and Silverlight(又名 PRISM)项目的工作人员,已经放弃了标准的 WPF 命令,转而使用更轻量级、更易于使用的基于委托的命令。ViewModel 只公开一个 ViewModel 属性,View 可以绑定到该属性。ViewModel 包含在 View 绑定的 ICommand 执行时运行的委托。基于委托的命令还使得可以确定命令的执行状态并在 View 中显示。下面是一个例子,我们使用 Predicate<object> 作为 ICommandCanExecute 处理程序,使用 Action<object> 作为 ICommandExecute 委托。

ViewModel 代码
/// <summary>
/// Simple ViewModel for the LoginControl
/// </summary>
public class LoginViewModel : ViewModelBase
{
    #region Data
    ....
    ....
    private ICommand loginCommand = null;
    private ICommand registerCommand = null;
    #endregion

    #region Ctor
    public LoginViewModel(IView view)
    {

        this.view = view;

        //wire up loginCommand
        loginCommand = new SimpleCommand
        {
            CanExecuteDelegate = x => !IsBusy,
            ExecuteDelegate = x => Login()
        };

        //wire up registerCommand
        registerCommand = new SimpleCommand
        {
            CanExecuteDelegate = x => !IsBusy,
            ExecuteDelegate = x => Register()
        };
    }
    #endregion

    #region Public Properties
    public ICommand RegisterCommand
    {
        get { return registerCommand; }
    }


    public ICommand LoginCommand
    {
        get { return loginCommand; }
    }

    ....
    ....
    #endregion
}
View 代码
<StackPanel Orientation="Horizontal">
    <Button x:Name="btnLogin" Content="Login"
            Height="30" Margin="10" 
            Style="{StaticResource GelButton}"
            ToolTip="Login Using Your Previous Details"                    
            Command="{Binding Path=LoginCommand}"/>
    <Button x:Name="btnRegister" Content="Register"
            Height="30" Margin="10" 
            ToolTip="Register As A New User"                    
            Style="{StaticResource GelButton}"
            Command="{Binding Path=RegisterCommand}"/>
</StackPanel>

这使用了我的好朋友 Marlon GrechSimpleCommand 代码,如下所示:

using System;
using System.Windows.Input;

namespace GeoPlaces
{
    /// <summary>
    /// Implements the ICommand and wraps up all the verbose 
    /// stuff so that you can just pass 2 delegates 1 for the 
    /// CanExecute and one for the Execute
    /// </summary>
    public class SimpleCommand : ICommand
    {
        /// <summary>
        /// Gets or sets the Predicate to execute when the 
        /// CanExecute of the command gets called
        /// </summary>
        public Predicate<object> CanExecuteDelegate { get; set; }

        /// <summary>
        /// Gets or sets the action to be called when the 
        /// Execute method of the command gets called
        /// </summary>
        public Action<object> ExecuteDelegate { get; set; }

        #region ICommand Members

        /// <summary>
        /// Checks if the command Execute method can run
        /// </summary>
        /// <param name="parameter">THe command parameter to 
        /// be passed</param>
        /// <returns>Returns true if the command can execute. 
        /// By default true is returned so that if the user of 
        /// SimpleCommand does not specify a CanExecuteCommand 
        /// delegate the command still executes.</returns>
        public bool CanExecute(object parameter)
        {
            if (CanExecuteDelegate != null)
                return CanExecuteDelegate(parameter);
            return true;// if there is no can execute default to true
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        /// <summary>
        /// Executes the actual command
        /// </summary>
        /// <param name="parameter">THe command parameter 
          /// to be passed</param>
        public void Execute(object parameter)
        {
            if (ExecuteDelegate != null)
                ExecuteDelegate(parameter);
        }

        #endregion
    }
}

接口用法:IView

秉承 MVVM 模式的精神,我们不想用额外的代码污染代码隐藏,因为这些代码显然现在由 ViewModel 代码完成。唯一的问题是,有时 ViewModel 需要执行一些 UI 操作,例如显示 MessageBox。相信我,您确实想坚持 ViewModel 方法,因为它创建了非常干净、分离/易于测试的代码。然而,上面概述的 MessageBox 情况是一个问题;我们该怎么办?

嗯,一种方法是让 View 公开服务,例如 MessageBox 服务。我的一个 WPF 同好 WPF DisciplesBill Kempf,正在一个名为 WPF Onyx 的伟大项目上做这项工作,我将在本文之后继续研究它。Onyx 使用这种基于服务的方法来真正帮助您创建使用 MVVM 模式的出色 WPF 应用程序。

总之,对于这个演示应用程序,我只是将 View 传递给 ViewModel,然后 ViewModel 可以调用 View 上暴露的、由已知接口公开的服务。让我们看一个例子,使用上面概述的 MessageBox 问题。请注意,View 实现了一个 IView 接口,该接口公开了一个 MessageBox 服务方法,ViewModel 可以调用该方法来告诉 View 做某事。同样,这避免了 View 代码隐藏中的混乱代码,因为 ViewModel 只能通过已知的、公开的服务接口与 View 通信。

View 代码
/// <summary>
/// A simple Login control that allows existing
/// users to login, or new users to register
/// </summary>
public partial class LoginControl : UserControl, IView
{
    #region Data
    private LoginViewModel loginViewModel = null;
    #endregion

    #region Ctor
    public LoginControl()
    {
        InitializeComponent();
        this.Loaded += new RoutedEventHandler(LoginControl_Loaded);
        loginViewModel = new LoginViewModel(this);
    }
    #endregion

    #region Private Methods
    private void LoginControl_Loaded(object sender, RoutedEventArgs e)
    {
        this.DataContext = loginViewModel;
    }
    #endregion

    #region IView Members

    public void ShowMessage(string message)
    {
        MessageBox.Show(message,"information",
            MessageBoxButton.OK,MessageBoxImage.Information);
    }

    #endregion

}

这是 ViewModel 代码。请注意,构造函数接受一个 IView 作为参数,当 ViewModel 想要显示 MessageBox 时,它会请求其内部的 IView 实例来执行此操作,它将使用我们上面看到的 MessageBox 服务方法。这样,ViewModel 就可以执行 UI 类型的事情,但本身不包含任何 UI 代码;View 完成所有这些工作。ViewModel 只是通过一个已知的接口请求 View 执行某项操作,View 就会执行。这个概念非常强大,它实现了表示层和驱动 View 的实际模型之间的极其清晰的代码分离。我非常喜欢这个主意,迫不及待地想和 WPF DisciplesBill Kempf 的宠物项目 WPF Onyx 一起开始一个大型文章。您肯定会从我这里看到更多关于 WPF Onyx 的内容;它看起来非常有前途。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Windows.Input;
using GeoPlacesModel;

namespace GeoPlaces
{
    /// <summary>
    /// Simple ViewModel for the LoginControl
    /// </summary>
    public class LoginViewModel : ViewModelBase
    {
        ....
        ....
        ....
        private IView view = null;

        #region Ctor
        public LoginViewModel(IView view)
        {
            this.view = view;
        }
        #endregion

        #region Private Methods
        private void Login()
        {
          .......
          .......
          view.ShowMessage(String.Format(
             "Could not log in user {0}, please try again",
             userName));
          .......
          .......

        }
        #endregion
    }
}

Mediator:中介者模式

可以想象,如果单个 View 被抽象为单个 ViewModel,那么就会出现 View/ViewModel 需要相互通信的情况。嗯,这是一个问题,因为 View 彼此不知道,ViewModel 也不知道彼此,那么我们如何执行 ViewModel 之间的消息传递呢?

幸运的是,通过使用 Mediator 模式可以获得帮助。Mediator 模式可以被看作是一个事件聚合器,它知道消息以及消息结果应该路由给谁。基本思想是,对于每条消息,那些有兴趣在特定消息的数据状态变化后获得回调的人将注册对特定消息的兴趣。当消息数据状态发生变化时,那些注册了对已更改消息数据感兴趣的人将通过某种回调形式收到通知。

那么这一切如何转化为代码呢?嗯,这相当简单;再次感谢 Action<T> 委托,我们有一个 Mediator 类,如下所示:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace GeoPlaces
{
    /// <summary>
    /// Available cross ViewModel messages
    /// </summary>
    public enum ViewModelMessages { IsAuthenticatedUser = 1, NewPlaceAdded=2 };

    /// <summary>
    /// A message Mediator singleton to allow unconnected ViewModel to 
    /// send and receive messages
    /// </summary>
    public sealed class Mediator
    {
        #region Data
        static readonly Mediator instance = new Mediator();
        private volatile object locker = new object();

          //specialized Dictionary (see the code for this)
        MultiDictionary<ViewModelMessages, Action<Object>> internalList
            = new MultiDictionary<ViewModelMessages, Action<Object>>();
        #endregion

        #region Ctor
        static Mediator()
        {
        }

        private Mediator()
        {

        }
        #endregion

        #region Public Properties


        public static Mediator Instance
        {
            get
            {
                return instance;
            }
        }

        #endregion

        #region Public Methods
        /// <summary>
        /// Registers a Colleague to a specific message
        /// </summary>
        /// <param name="callback">The callback to use when the message it seen</param>
        /// <param name="message">The message to register to</param>
        public void Register(Action<Object> callback, ViewModelMessages message)
        {
            internalList.AddValue(message, callback);
        }

        /// <summary>
        /// Notify all colleagues that are registed to the specific message
        /// </summary>
        /// <param name="message">The message for the notify by</param>
        /// <param name="args">The arguments for the message</param>
        public void NotifyColleagues(ViewModelMessages message, object args)
        {
            if (internalList.ContainsKey(message))
            {
                //forward the message to all listeners
                foreach (Action<object> callback in internalList[message])
                    callback(args);
            }
        }
        #endregion
    }
}

可以看到,Mediator 只是维护一个消息和回调的 Dictionary,并且当消息数据状态发生变化时,回调 Action<Object> 委托会被调用,并将新状态作为 Object 传递给回调 Action<Object> 委托。这是通过 Mediator 的 NotifyCollegues 方法完成的,该方法查找所有已注册的回调 Action<Object> 委托,并调用它们,传递新的状态对象。因此,所有希望相互通信的 ViewModel 都将使用 Mediator 的 Register() 方法,并通过注册消息数据状态更改时使用的 Action<Object> 委托接收回调。

这是 ViewModel 代码注册 Mediator 的样子:

//register to the mediator for the IsAuthenticatedUser message
Mediator.Instance.Register(
    (Object o) =>
    {
        isAuthenticatedUser = (Boolean)o;
        if (isAuthenticatedUser)
            GetAllPlaces();
    }, ViewModelMessages.IsAuthenticatedUser);

下面是一个更新消息数据状态的示例,并通知感兴趣的各方新数据状态可用:

Mediator.Instance.NotifyColleagues(
    ViewModelMessages.IsAuthenticatedUser, true);

在此示例中,您可以看到回调委托实际定义如下,并且我们必须将传递给回调 Action<Object> 委托的 Object 参数转换为预期的类型(在本例中为 Boolean)。

(Object o) =>
{
    isAuthenticatedUser = (Boolean)o;
    if (isAuthenticatedUser)
        GetAllPlaces();
}

我认为这种方法效果很好。

服务代理:WCF 服务代理

正如在 RESTful WCF 服务 部分提到的,RESTful WCF 使用 HTTP。这很棒,但十次中有九次,您不只是想 GET 数据;您会想 POST 和 DELETE 数据。在这种情况下,您将不得不使用某种 API 来处理 HTTP,或者您可以使用 WebChannelFactory 类,它允许您以与使用 SOAP 基于 WCF 服务类似的方式使用 RESTful 服务。

WebChannelFactory 类是一个特殊的 ChannelFactory,如果端点尚未包含 WebHttpBehavior,它会自动添加它。此外,如果未显式配置绑定且地址是 HTTP 或 HTTPS 地址,它将向端点添加默认的 WebHttpBinding

所以您可以看到 WebChannelFactory 类为我们的客户端代码完成了大部分繁重的工作。我们所需要做的就是创建一个新的 WebChannelFactory,这几乎就完成了工作。当然,我们是优秀开发者,我们希望创建健壮的可重用代码,将客户端 ChannelFactory 的概念抽象成更自然的东西。

为此,我开发了以下 RESTful WCF 服务代理类,它使使用 WebChannelFactory 的整个过程变得更加容易,并提供错误处理。

using System;
using System.Configuration;
using System.Net;
using System.ServiceModel;
using System.ServiceModel.Web;
using GeoPlacesDataService;
using Microsoft.ServiceModel.Web;

namespace GeoPlaces
{
    /// <summary>
    /// The client proxy delegate, which is typically an anonomous delegate
    /// in the actual client code
    /// </summary>
    public delegate void UseServiceDelegate(IGeoService proxy);

    /// <summary>
    /// This section of code was originally obtained from the following source
    /// http://www.iserviceoriented.com/blog/post/Indisposable+-+WCF+Gotcha+1.aspx
    /// 
    /// It was subsequently changed in order to make it work with the web based
    /// RestFul WCf service. This class should handle restarting the proxy in case
    /// of a faulted channel
    /// </summary>
    public static class Service
    {
        #region Data

        private static IClientChannel proxy = null;
        public static ChannelFactory<IGeoService> _channelFactory = null;

        #endregion

        static Service()
        {
            try
            {
                Uri serviceUri = new Uri(
                    ConfigurationManager.AppSettings["GeoServiceEndpointAddress"]);
                _channelFactory = new WebChannelFactory<IGeoService>(serviceUri);
                _channelFactory.Endpoint.Behaviors.Add(
                    new ContentTypeBehaviour { ContentType = "text/xml" });
            }
            catch (Exception e)
            {
                ApplicationException ae = new ApplicationException(
                    "Error initiating WCF channel for The GeoService", e);
                Console.WriteLine(String.Format("An exception occurred : {0}", ae));
                throw ae;
            }
        }


        #region Public Methods
        public static void Use(UseServiceDelegate codeBlock)
        {
            bool success = false;

            if (proxy != null && (proxy.State == CommunicationState.Opened ||
                                  proxy.State == CommunicationState.Opening))
            {
                //do nothing, all ok
            }
            else
                proxy = (IClientChannel)_channelFactory.CreateChannel();

            //try to create the Proxy
            try
            {
                codeBlock((IGeoService)proxy);
                success = true;
            }


            //WebException is only avaiable WCF REST Starter Kit Preview 2
            //http://aspnet.codeplex.com/Release/ProjectReleases.aspx?ReleaseId=24644
            catch (WebProtocolException webExp)
            {
                Console.WriteLine(String.Format("An exception occurred : {0}",
                    webExp.Message));

                throw new ApplicationException(
                    "A GeoService WebProtocolException occured", webExp);
            }
            catch (WebException ex)
            {
                using (System.IO.Stream respStream = ex.Response.GetResponseStream())
                    using(System.IO.StreamReader reader = 
                        new System.IO.StreamReader(respStream))
                            Console.WriteLine(String.Format("An exception occurred : {0}",
                            reader.ReadToEnd()));
            }
            catch (FaultException fex)
            {
                Console.WriteLine(String.Format("An exception occurred : {0}",
                    fex.Message));
                throw new ApplicationException(
                    "A GeoService FaultException occured", fex);
            }
            catch (Exception ex)
            {
                Console.WriteLine(String.Format("An exception occurred : {0}",
                    ex.Message));
                throw new ApplicationException(
                    "A GeoService Exception occured", ex);
            }
            finally
            {
                if (!success && proxy != null)
                    proxy.Abort();
            }
        }
        #endregion
    }
}

需要注意的一点是 WebProtocolException,您在标准 .NET 代码库中找不到它。这是一个额外的类,属于 WCF REST 入门工具包 Preview 2。该入门工具包包含一些可以帮助您开始使用 RESTful WCF 的好类。这个入门工具包不是必需的,因为我已经将必要的 DLL 包含在此应用程序的代码库中。

您将在下面看到如何使用这个代理辅助类:

服务调用:Making Service Calls

正如我刚才所说,我开发了一个易于使用的 RESTful WCF 代理类,它极大地简化了 WebChannelFactory 的使用。那么我们该如何使用这个辅助类呢?嗯,这其实很简单。让我们看一些针对演示代码的 RESTful WCF 服务执行的 POST/GET 示例,好吗?

AuthenticateUser (POST)

它在 RESTful WCF 服务中定义如下:

[OperationContract]
[WebInvoke(Method = "POST", UriTemplate = "User/Add/",
    ResponseFormat = WebMessageFormat.Xml)]
Users AddUser(Users newUser);

并且其实现如下面的代码所示,我们将新用户持久化到 SQL Server 数据库,使用了 ADO.NET 实体框架。我还使用了一些定义来确定是否需要创建 HTTP 响应代码并为新资源创建新的 REST URI(这很礼貌;用户可能实际上正在使用浏览器,因此会赞赏为用户新添加的资源创建新的 URI)。

/// <summary>
/// Adds a user to the System
/// </summary>
public Users AddUser(Users newUser)
{
    try
    {
        GeoPlacesEntities model = new GeoPlacesEntities();
        model.AddToUsers(newUser);
        model.SaveChanges();

        Users userFromDb = model.Users.Where((u) =>
           u.Name.ToLower().Equals(newUser.Name) &&
           u.Password.ToLower().Equals(newUser.Password)
           ).First();

#if HTTP
        OutgoingWebResponseContext ctx = 
            WebOperationContext.Current.OutgoingResponse;
        ctx.SetStatusAsCreated(CreateNewUserUri(userFromDb));
#endif

        return userFromDb;
    }
    catch(Exception ex)
    {
        //WebProtocolException is part of WCF REST Starter Kit Preview 2
        throw new WebProtocolException(HttpStatusCode.BadRequest,
            "Couldn't add new user", null);
    }
}

那么,让我们看看 WPF 客户端代码,使用我们的代理助手来调用它:

/// <summary>
/// Logs a user in, and returns the logged in user
/// 
/// Calls WCF Service method
/// [OperationContract]
/// [WebInvoke(Method = "POST", UriTemplate = "User/Add/",
///     ResponseFormat = WebMessageFormat.Xml)]
/// Users AddUser(Users newUser);
/// </summary>
public static Users RegisterUser(String username, String password)
{
    Boolean isAuthenticatedUser = false;

    Users currentUser = new Users
    {
        Name = username,
        Password = password,
        Places = null
    };

    Users dbReadUser = null;

    try
    {
        //Use the GEOPlacesService Proxy
        Service.Use((client) =>
        {
            //need OperationContextScope to use WebOperationContext(s)
            //and HttpStatusCode(s)
            using (new OperationContextScope((IContextChannel)client))
            {

                dbReadUser = client.AddUser(currentUser);

                IncomingWebResponseContext rctx =
                    WebOperationContext.Current.IncomingResponse;
                if (rctx.StatusCode == System.Net.HttpStatusCode.Created)
                {
                    if (dbReadUser != null)
                        isAuthenticatedUser = dbReadUser.ID >= 0;
                }
            }

        });

        if (isAuthenticatedUser)
            return dbReadUser;
        else
        {
            Console.WriteLine("Error registering user");
            return null;
        }
    }
    catch (ApplicationException Ex)
    {
        return null;
    }
}

看起来像下面代码的那一行就是代理助手的使用,其中客户端是实际的 IClientChannel,它在服务代理助手类中创建。您看到了吗?它再次使用了委托,因此运行的代码是在服务代理助手类中创建的 IClientChannel 上执行的。另请注意,我们必须使用 OperationContextScope 来允许我们检查 HTTP 状态代码。

Service.User((client) => { }

为了完整起见,我们还考虑一个 GET 调用。

GetAllPlacesForUser (GET)

它在 RESTful WCF 服务中定义如下:

[OperationContract]
[WebGet(UriTemplate = "/placesList/{userId}",
    ResponseFormat = WebMessageFormat.Xml)]
List<Places> GetAllPlacesForUser(String userId);

它具有如下所示的实现,其中我们使用 ADO.NET 实体框架从 SQL Server 数据库获取用户的所有地点。

/// <summary>
/// Gets all places for a particular user
/// </summary>
public List<Places> GetAllPlacesForUser(String userId)
{
    Int32 id = -1;
    if (Int32.TryParse(userId, out id))
    {
        try
        {
            GeoPlacesEntities model = new GeoPlacesEntities();
            return model.Places.Where((pl) => pl.Users.ID == id).ToList();
        }
        catch (Exception ex)
        {
            //WebProtocolException is part of WCF REST Starter Kit Preview 2
            throw new WebProtocolException(HttpStatusCode.BadRequest,
                String.Format("Couldn't find places for user id {0}", userId), null);

        }
    }
    else
        return null;
}

同样,我们使用服务代理辅助类,这行代码与之前一样,我们必须使用 OperationContextScope 来允许我们检查 HTTP 状态代码。

Service.User((client) => { }
/// <summary>
/// Logs a user in, and returns the logged in user
/// 
/// Calls WCF Service method
/// [OperationContract]
/// [WebGet(UriTemplate = "/placesList/{userId}",
///    ResponseFormat = WebMessageFormat.Xml)]
/// List<Places> GetAllPlacesForUser(String userId);
/// </summary>
public static ObservableCollection<Places> GetAllPlacesForUser(String userId)
{
    Boolean completedOk = false;
    ObservableCollection<Places> places = null;
    List<Places> placesReturned = null;

    try
    {
        //Use the GEOPlacesService Proxy
        Service.Use((client) =>
        {
            //need OperationContextScope to use WebOperationContext(s)
            //and HttpStatusCode(s)
            using (new OperationContextScope((IContextChannel)client))
            {
                placesReturned = client.GetAllPlacesForUser(userId.ToString());

                IncomingWebResponseContext rctx =
                    WebOperationContext.Current.IncomingResponse;
                if (rctx.StatusCode == System.Net.HttpStatusCode.OK)
                {
                    completedOk = placesReturned != null;
                }
            }

        });

        if (completedOk)
        {
            places = new ObservableCollection<Places>(placesReturned);
            return places;
        }
        else
        {
            Console.WriteLine("Error getting all places for user");
            return null;
        }
    }
    catch (ApplicationException Ex)
    {
        return null;
    }
}

所有 ViewModel 都通过与上面所示的类似方法与 RESTful WCF 服务通信,其中所有实际的 WCF 服务调用都通过另一个名为“ServiceCalls”的辅助类完成,该类执行上面所示的 RESTful WCF 服务调用,并允许 ViewModel 代码保持干净,不受任何 WCF 服务层代码/using 语句的干扰。

VE 托管实例:VE 地图

正如本文中在多个地方所述,我正在使用一个托管的 Microsoft Virtual Earth 实例,这显然意味着您必须安装 Microsoft Virtual Earth。但是这个看起来像这样的控件呢?它是如何工作的?

嗯,实际上有点作弊;它是一个 WPF 控件。我没有编写这个控件,但我对它足够了解,可以谈论它。它来自 InfoStrat,可以在 Virtual Earth 3D for WPF and Microsoft Surface 上找到(同样,我的附加代码包含您需要的所有内容,除非您想自己下载,否则无需下载)。

InfoStrat.VE 控件使用内部的 D3DImage,这是一个 WPF 控件,可用于在 WPF 应用程序中托管 DirectX 3D 内容。

所以 Infrostrat 实际上是使用一个 IntPtr 句柄来指向 DirectX 渲染到 D3DImage(我刚开始使用 Google Earth 的 COM API 时使用了非常相似的想法)。这是 InfoStrat.VE.Map 代码中最重要的部分,我们获得了 VE DirectX 指针到将被托管在基于 WPF 的 D3DImage 上的 3D 曲面。

/// <summary>
/// Gets pointer to the Virtual Earth D3D backbuffer
/// </summary>
/// <returns></returns>
private IntPtr GetSourceSurfacePtr()
{
    GraphicsEngine3D graphicsEngine = GetGraphicsEngine();

    if (graphicsEngine == null)
        return IntPtr.Zero;

    Microsoft.MapPoint.Graphics3D.Types.Surface surfSrc = null;

    IntPtr ret = IntPtr.Zero;

    try
    {

        surfSrc = graphicsEngine.Device.GetRenderTarget(0);

        if (surfCpy == null)
        {
            CreateVESurface();
        }

        if (surfSrc != null && surfCpy != null)
        {
            graphicsEngine.Device.StretchRectangle(surfSrc, 
                new System.Drawing.Rectangle(0, 0, surfSrc.Description.Width, 
                    surfSrc.Description.Height), surfCpy, 
                new System.Drawing.Rectangle(0, 0, surfSrc.Description.Width, 
                    surfSrc.Description.Height));

            Microsoft.MapPoint.GraphicsAPI.Graphics.Surface internalSurf = null;

            internalSurf = 
               ReadPrivateVariable<Microsoft.MapPoint.Graphics3D.Types.Surface,
               Microsoft.MapPoint.GraphicsAPI.Graphics.Surface>(surfCpy, 
               "internalSurface");

            if (internalSurf != null)
            {

                //NativePointer is hidden from intellisense
                ret = internalSurf.NativePointer;
            }
        }
    }
    finally
    {
        if (surfSrc != null)
            surfSrc.Dispose();
    }

    return ret;
}

可能,使用此 D3DImage 控件的最佳教程是由 Dr. WPF 提供的(所以您知道它是 1,000,000% 正确的),可以在 D3DImage.aspx 上找到。

因此,通过使用 InfoStrat.VE.Map 控件,剩下的就是创建一个控件实例并添加一些可以执行各种地图功能的按钮,例如:

  • 使用提供的按钮放大/缩小
  • 使用提供的按钮或仅使用鼠标进行平移(鼠标很有趣,而且非常令人满意;旋转世界从未感觉如此之好;我现在知道阿特拉斯的感觉了)
  • 模式更改为:混合/仅路线或仅景观,使用提供的按钮。

这些都是在代码隐藏中完成的。我知道,我知道,这很不 MVVM,不是吗?嗯,在这种情况下,它似乎不太适合 MVVM 模式。这是所有这些地图功能的代码。我应该指出,我不得不在代码隐藏中创建实际的 InfoStrat.VE.Map,因为有时即使应用程序编译并运行正常,IntelliSense 也会丢失。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using InfoStrat.VE;
using System.Linq;
using GeoPlacesModel;

namespace GeoPlaces
{
    /// <summary>
    /// Virtual Earth control and a List of Places
    /// </summary>
    public partial class VEMapControl : UserControl, IView
    {
        #region Data
        private PlacesViewModel placesViewModel = null;
        private VEMap map = null;
        private VEPushPin newPin = null;
        #endregion

        #region Ctor
        public VEMapControl()
        {
            InitializeComponent();
            SetUpMap();
            this.Loaded += new RoutedEventHandler(VEMapControl_Loaded);
            placesViewModel = new PlacesViewModel(this);
            map.Show3DCursor = true;

            //wire up PlaceClicked
            this.AddHandler(PlaceControlDetailed.PlaceClickedEvent, 
                new PlaceClickedRoutedEventHandler(PlaceClicked));
        }
        #endregion

        #region Private Methods
        /// <summary>
        /// Had to resort to setting up Map in code, as intellisense
        /// was being lost (though all was ok at runtime) if map was
        /// set up in XAML
        /// </summary>
        private void SetUpMap()
        {
            //Create the VE Map
            map = new VEMap
              {
                  Width = 590,
                  Height = 590,
                  Margin = new Thickness(5),
                  VerticalAlignment = VerticalAlignment.Top,
                  HorizontalAlignment = System.Windows.HorizontalAlignment.Center,
                  MapStyle = VEMapStyle.Hybrid,
                  LatLong = new Point(38.9444195081574, -77.0630161230201),
                  Clip = new EllipseGeometry
                     {
                         RadiusX = 230,
                         RadiusY = 230,
                         Center = new Point(295, 295)
                     }
              };

            //Ceeate a default pin location (my house)
            newPin = new VEPushPin(new VELatLong(50.826958333333337, 
                -0.16388055555555556));
                
            newPin.SetResourceReference(VEPushPin.StyleProperty, "PushPinStyle");
            newPin.Content = new Label
            {
                Content = "Waiting",
                HorizontalAlignment = HorizontalAlignment.Center,
                FontSize = 20
            };
            newPin.Click += VEPushPin_Click;
            map.Items.Add(newPin);


            //I do not like doing this with indexes that may change, but
            //I had no choice as I wanted map to be exactly 4th child, and 
            //when setting up map in XAML it would sometimes loose intellisense
            mainGrid.Children.Insert(4,map);
        }


        private void VEMapControl_Loaded(object sender, RoutedEventArgs e)
        {
            this.DataContext = placesViewModel;
        }

        private void btnZoomIn_Click(object sender, RoutedEventArgs e)
        {
            map.DoMapZoom(1000, false);
        }

        private void btnZoomOut_Click(object sender, RoutedEventArgs e)
        {
            map.DoMapZoom(-1000, false);
        }

        private void BtnRoad_Click(object sender, RoutedEventArgs e)
        {
            map.MapStyle = InfoStrat.VE.VEMapStyle.Road;
        }
        private void BtnAerial_Click(object sender, RoutedEventArgs e)
        {
            map.MapStyle = InfoStrat.VE.VEMapStyle.Aerial;
        }
        private void BtnHybrid_Click(object sender, RoutedEventArgs e)
        {
            map.MapStyle = InfoStrat.VE.VEMapStyle.Hybrid;
        }
  
        private void VEPushPin_Click(object sender, VEPushPinClickedEventArgs e)
        {
            VEPushPin pin = sender as VEPushPin;
            map.FlyTo(pin.LatLong, -90, 0, 300, null);
        }

        private void btnPanUp_Click(object sender, RoutedEventArgs e)
        {
            map.DoMapMove(0, 1000, false);
        }

        private void btnPanDown_Click(object sender, RoutedEventArgs e)
        {
            map.DoMapMove(0, -1000, false);
        }

        private void btnPanLeft_Click(object sender, RoutedEventArgs e)
        {
            map.DoMapMove(1000, 0, false);
        }

        private void btnPanRight_Click(object sender, RoutedEventArgs e)
        {
            map.DoMapMove(-1000, 0, false);
        }

        /// <summary>
        /// Remove old pin and add new pin when user selects a place to view
        /// </summary>
        private void PlaceClicked(Object sender, PlaceClickedEventArgs args)
        {
            Places selectedPlace = args.PlaceSelected;
            newPin.Latitude = selectedPlace.Latitude;
            newPin.Longitude = selectedPlace.Longitude;
            newPin.Content = new Label
            {
                Content = selectedPlace.Name,
                HorizontalAlignment = HorizontalAlignment.Center,
                FontSize = 20
            };
            map.FlyTo(new VELatLong(selectedPlace.Latitude, 
                selectedPlace.Longitude), -80, 0, 300, null);
        }
        #endregion

        #region IView Members

        public void ShowMessage(string message)
        {
            MessageBox.Show(message, "information",
                MessageBoxButton.OK, MessageBoxImage.Information);
        }

        #endregion

    }
}

这里唯一真正重要的是设置了一个事件处理程序来处理之前从 SQL 保存的用户地点被点击。当一个地点从地点列表中被点击时,地图针会移动到新的纬度/经度位置,并且地图会飞到新的图钉位置。我觉得这很酷。

哦,有一点值得一提的是,默认情况下,InfoStrat.VE.Map 控件是正方形的,但通过使用 Ellipse 进行裁剪,我们可以有效地将控件裁剪为 Ellipse

Google Earth API 略微偏离

当我开始写这篇文章时,我正在使用 Google Earth 的 COM API 并将其托管在 WinForms 控件中,然后通过 Interop 托管在 WPF 应用程序中,我使用以下代码完成的,如果有兴趣的话。当我看到 Infostrat.VE.Map 的东西时,Google Earth 的 COM API 似乎有点老旧和 hacky。但我认为 Google Earth 的 Web 控件更好。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;

namespace GoogleEarthControl
{
    public class Win32
    {
        [DllImport("user32", CharSet = CharSet.Auto)]
        public extern static IntPtr GetParent(IntPtr hWnd);

        [DllImport("user32", CharSet = CharSet.Auto)]
        public extern static bool MoveWindow(IntPtr hWnd, 
            int X, int Y, int nWidth, int nHeight, bool bRepaint);

        [DllImport("user32", CharSet = CharSet.Auto)]
        public extern static IntPtr SetParent(IntPtr hWndChild, 
            IntPtr hWndNewParent);

        [DllImport("user32", CharSet = CharSet.Auto)]
        public extern static IntPtr PostMessage(int hWnd, 
            int msg, int wParam, int IParam);

        [DllImport("user32", CharSet = CharSet.Auto)]
        public extern static bool SetWindowPos(int hWnd, 
            IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);

        [DllImport("coredll.dll", 
              CharSet = CharSet.Auto, SetLastError = false)]
        public static extern IntPtr SendMessage(IntPtr hWnd, 
                      uint Msg, IntPtr wParam, IntPtr lParam);


        public static readonly Int32  WM_QUIT           = 0x0012;
        public static readonly IntPtr HWND_TOP          = new IntPtr(0);
        public static readonly IntPtr HWND_BOTTOM       = new IntPtr(1);
        public static readonly UInt32 SWP_HIDEWINDOW    = 128;
        public static readonly UInt32 SWP_SHOWWINDOW    = 64;
        public static readonly uint   WM_SYSCOMMAND     = 0x0112;
        public static readonly int    SC_CLOSE          = 0xF060;

        public static IntPtr GEHrender = (IntPtr)5;
    }
}

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Data;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using EARTHLib;
using System.Runtime.InteropServices;

namespace GoogleEarthControl
{
    public partial class WinFormGEContainerControl : UserControl
    {

        private ApplicationGEClass googleEarth;
        private IntPtr mainWindowPtr = (IntPtr)(-1);

        public WinFormGEContainerControl()
        {
            InitializeComponent();
        }

        private void WinFormGEContainerControl_Load(object sender, EventArgs e)
        {
            mainWindowPtr = this.Handle;
            googleEarth = new ApplicationGEClass();
            Win32.GEHrender = (IntPtr)googleEarth.GetRenderHwnd();
            Win32.MoveWindow(Win32.GEHrender, 0, 0, (int)this.Width, 
               (int)this.Height, true);
            Win32.SetParent(Win32.GEHrender, mainWindowPtr);
            Win32.SetWindowPos(googleEarth.GetMainHwnd(), Win32.HWND_BOTTOM,
                10, 10, 10, 10, Win32.SWP_HIDEWINDOW);
        }

        public void StopGE()
        {
            try
            {
                Win32.SendMessage((IntPtr)googleEarth.GetMainHwnd(), 
                     Win32.WM_SYSCOMMAND,
                    (IntPtr)Win32.SC_CLOSE, (IntPtr)0);
            }
            catch (Exception)
            {
                //Ok P/Invoke close didn't work, so have no choice but to kill process
                Process[] p = Process.GetProcessesByName("googleearth");
                if (p.Length > 0)
                {
                    try
                    {
                        p[0].Kill();
                    }
                    catch (Exception)
                    {
                        Console.WriteLine(
                          "There was a problem shutting down googleearth");
                    }
                }
            }
            finally
            {
                try
                {
                    if (googleEarth != null)
                            Marshal.ReleaseComObject(googleEarth);
                }
                catch (ArgumentException argEx)
                {
                    Console.WriteLine("There was a problem shutting down googleearth");
                }
            }
        }
    }
}

3D

正如我前面提到的,WPF 客户端使用了 3D 翻转控件,它允许用户在 3D 网格的前面和后面托管两个交互式控件,并允许用户在 3D 空间中翻转它。我实际上在我的 MyFriends 应用中也用过这种技术,它很酷,但有点混乱,并且需要用户了解一些关于 3D 的基本知识。

现在,幸运的是,我的一位 WPF 偶像/朋友和 WPF 爱好者 Josh Smith 先生,在将它抽象成一个简单易用的 3D WPF 控件方面做得非常出色。Josh 将他的 3D 库称为 Thriple,您可以在 Thriple 网站上阅读所有相关内容。

样式/模板

当然,WPF 客户端大量使用样式/模板,但这都是非常标准的 WPF 内容;如果您想了解这方面的内容,只需查看代码即可。

有一个相当不错的脉动环控件,巧妙地命名为“PulsingRingControl”。

就是这样,各位

我想提一下,这篇文章我花了大约一个月的时间在业余时间写完的,所以任何投票/评论都将不胜感激。总之,我希望您能从这篇文章中学到一些东西,并希望它教会了您一点知识。

就像我说的,请继续关注更多关于 WPF Onyx 的文章;它是一个非常有前途的框架,Bill 干得好!

有用链接

© . All rights reserved.