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

使用 WCF/WPF 演示代码进行动态 LINQ to Entities 查询

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (83投票s)

2008年11月30日

CPOL

22分钟阅读

viewsIcon

260763

downloadIcon

4526

演示了跨 WCF 服务边界进行动态查询的方法。

目录

引言

我已经有一段时间没有撰写大型文章了,所以我想是时候纠正这一点,写一篇内容丰富而扎实的文章了。这篇文章就代表了这种扎实。

我想我应该先说明这篇文章将涵盖什么。实际上,它将涵盖很多内容。基本思路是,某个地方有一个安装了 Northwind 数据库的 SQL Server 数据库,我们将使用 ORM 通过 WCF 服务从该数据库获取数据。WCF 本身将托管在 Windows 服务中,该服务将通过自定义安装程序进行安装。

为了与这个 Windows 托管的 WCF 服务交互,我们将使用一个 WPF 客户端应用程序。WPF 客户端应用程序将允许用户使用自定义查询构建器查询特定实体(仅限客户,因为如果允许所有实体,我将永远写这篇文章),然后该查询构建器将用于发送到 Windows 托管的 WCF 服务,WCF 服务将反过来查询 ORM 以获取查询到的实体,并将结果返回给 WPF 客户端应用程序。

本质上,就是这样;它可能没有那么令人兴奋,但这里有足够的内容可以向您展示如何创建服务/客户端和托管 Windows 服务。此外,在此过程中,我将稍微绕道谈论某些实践和酷代码,这些可能会让您的生活更轻松。

我应该指出,UI 最初只是一个一次性的东西;好吧,我尝试让它看起来不错,但这只是因为我喜欢 WPF。我的意思是,其中仍然有一些不错的想法,比如查询的构建方式,可以移植到更丰富的查询生成器中,但这留给读者作为练习。

必备组件

您需要安装 VS2008(用于 Entity Framework)和 VS2008 SP1,以及 .NET 3.5 SP1。

运行此应用程序的准备工作

为了获得随附的代码(托管在 Windows 服务/WPF 客户端中的 WCF 服务),您需要确保已完成以下操作

  1. Windows 服务已安装并在安装过程中提供了登录详细信息。
  2. 关联的 .Config 文件的 LINQ to Entities connectionStrings 部分已更新,以指向您本地的 SQL Server 安装。
  3. 当您尝试使用 WPFClient 时,Windows 服务正在运行。
  4. WPFClient 项目中的 App.Config 已配置为使用正确的安全登录;这在 identity/userPrincipalName 元素中。

为了让您更熟悉这一切,这里是项目结构

查看演示

运行时它是什么样子?嗯,我决定给你们看一些截图,还附带一个关于这个的小视频链接

初始视图加载

更改所选客户订单的视图类型

开始构建动态查询

LINQ to Entities

对于 ORM,我最初考虑使用 LINQ to SQL,但是你知道,我认为微软正在慢慢地(个人观点)将其淘汰,转而支持 LINQ to Entities。所以我想为什么不使用 LINQ to Entities 呢。这就是本文用于 ORM 的技术。

我过去在 LINQ to Entities 还在 BETA 版时就安装过它,那真是太糟糕了;设计器曾经生成三部分 XML

正如我所看到的,设计器过去常常创建这些,但在过去做得并不好,你总是不得不亲自动手修改 XML,而且代码量巨大,一个小型数据库就有几千行。哎呀。

如果你想知道这有多糟糕,只需使用上面三个链接中的任何一个。

使用旧版 LINQ to Entities 测试版维护这三个 XML 部分简直是一场噩梦。幸运的是,新的设计器不再让用户直接担心 XML,我认为这是一件好事。现在的情况是,设计器的后台代码现在是一个 C#|VB 类,它有一个关联的 App.Config 文件,其中包含一个非常奇怪的 ConnectionStrings 部分。让我们来看看其中一个

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <connectionStrings>
    <!-- HOME -->
    <add name="NorthwindEntities" 
    connectionString="metadata=res://LINQ2Entities/Northwind.csdl|
    res://LINQ2Entities/Northwind.ssdl|
    res://LINQ2Entities/Northwind.msl;provider=System.Data.SqlClient;
    provider connection string="Data Source=VISTA01\SQLEXPRESS;
    Initial Catalog=Northwind;Integrated Security=True;
    MultipleActiveResultSets=True"" 
    providerName="System.Data.EntityClient" />

  </connectionStrings>
</configuration>

要运行随附的代码,您需要更改安装程序用于安装服务时文件夹内的关联配置;这将在文章后面的“Windows 服务托管和安装”部分中介绍。

我们可以看到,仍然有指向 CSDL/MSL/SSDL 的链接,但它们现在被视为元数据资源。好多了。

因此,如果我们将注意力集中在 LINQ to Entities 设计器上,我们可以看到它与 LINQ to SQL 设计器有些相似,但又略有不同。这是为了允许从多个视图/表构建实体。

但是,在这个例子中,我将使用实体与 Northwind 数据库表之间的一对一映射。

LINQ to Entities SQL 语言

LINQ to Entities 实际上比 LINQ to SQL 更进一步,因为它现在支持一种完整的 SQL 查询语言,据我所知,它实际上与 SQL 语法相同。这是一个小例子

using (AdventureWorksEntities advWorksContext =
       new AdventureWorksEntities())
{
    string esqlQuery = @"SELECT contactID, AVG(order.TotalDue) 
                            FROM AdventureWorksEntities.SalesOrderHeader 
                            AS order GROUP BY order.Contact.ContactID as contactID";

    try
    {
        foreach (DbDataRecord rec in
            new ObjectQuery<DbDataRecord>(esqlQuery, advWorksContext))
        {
            Console.WriteLine("ContactID = {0}  Average TotalDue = {1} ",
                rec[0], rec[1]);
        }
    }
    catch (EntityException ex)
    {
        Console.WriteLine(ex.ToString());
    }
    catch (InvalidOperationException ex)
    {
        Console.WriteLine(ex.ToString());
    }
}

现在,我们可以用查询存储 String 的事实有点好,因为它意味着我们可以跨服务边界传递查询,这是 LINQ to SQL 开箱即用无法做到的。我的意思是,你无法序列化一个 Expression<Func<Customers,Bool>>,也无法返回一个 var,因为 var 具有方法级别的范围。你也无法在客户端创建 LINQ 查询并序列化它们(尽管这会非常酷)并将它们发送到服务运行。因此,LINQ to SQL 绝对是受限的。所以,这种新的 String 查询能力乍一看似乎是一件好事。

但是,让我们思考一下。我们过去可以通过 ADO.NET 使用内联/跨线 SQL 来做这种事情,看看那是多么混乱,更不用说 SQL 注入攻击了。所以,人们不得不问这到底是不是一件好事。我的意思是,有什么能阻止恶意用户创建自己的查询字符串呢?它现在使用 Entity Framework 而不是实际数据库的事实对恶意用户来说并不重要,他们可能会得到相同的结果。

我认为更好的选择是仅仅通过使用动态生成的 Where 子句来限制搜索结果,或者,如果你有时间,创建一个 SQL 查询生成器,该生成器在实际需要创建查询字符串并将其传递给 DB|ORM 的最后一刻之前,从不包含完整的字符串。后一种方法是我们实际在工作中采用的方法;本文将实际讨论动态生成的 Where 子句解决方案。

动态 Where 子句

LINQ to Entities 实际上提供了一个 ObjectQuery<T>.Where 方法,该方法接受一个字符串和一个 ObjectParameter 数组。

根据 ObjectParameter 文档,以下代码应该可以工作。不幸的是,它似乎不喜欢 ObjectParameter 名称。

真烦人!噢,幸好,有办法解决。还记得我说的 LINQ to SQL 无法开箱即用地运行动态查询吗?嗯,那过去是/现在是事实。但是,有一个额外的微软构建的类,它具有 IQueryable<T> 扩展方法,允许 LINQ to Entities 和 LINQ to SQL 都创建动态 Where 子句。

所以,如果我们更改原始查询以使用 DynamicQuery API,我们可以成功运行此查询。但是,我们必须使用编号参数,这有点麻烦,但总比上面所示的 LINQ to Entities 的恐怖场景要好。我只能想象上面所示问题的原因是 LINQ to Entities 实现中有一些奇怪的解析,试图生成实际的 SQL,而 DynamicQuery API 将尝试创建 Expression<Func<Customers,Bool>>

这两种 API 有何不同

正如刚才提到的,在使用原生 LINQ to Entites 的 ObjectQuery<T>.Where 方法时,System.Data.Entity 类实际上会尝试生成可用于数据库的实际 SQL。

现在让我们看看 DynamicQuery API 如何工作。它提供了一个针对 IQueryable<T> 类型的扩展方法,并根据输入值创建一个表达式树。

以下是 DynamicQuery API 的一个小节

public static IQueryable<T> Where<T>(this IQueryable<T> source, 
       string predicate, params object[] values)
{
    return (IQueryable<T>)Where((IQueryable)source, predicate, values);
}

public static IQueryable Where(this IQueryable source, 
    string predicate, params object[] values)
{
    if (source == null) throw new ArgumentNullException("source");
    if (predicate == null) throw new ArgumentNullException("predicate");
    LambdaExpression lambda = 
        DynamicExpression.ParseLambda(source.ElementType, 
        typeof(bool), predicate, values);
    return source.Provider.CreateQuery(
        Expression.Call(
            typeof(Queryable), "Where",
            new Type[] { source.ElementType },
            source.Expression, Expression.Quote(lambda)));
}

你们中的一些人可能还记得我最近写了一篇关于 Expression Trees 的文章,并向你们展示了它们是如何工作的。这个 DynamicQuery API 是我写那篇文章的全部原因。

我希望您现在能明白为什么我们可能想在运行时创建表达式树。它允许我们基于表达式树创建动态查询。

WCF 服务

整体结构

WCF 服务实际上非常简单;整个 ServiceContract 如下所示

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

namespace Service.Portal
{
    [ServiceContract(
        Namespace = "http://www.sachabarber.net/2008/cp", 
        SessionMode = SessionMode.Allowed)]
    public interface IPortal
    {
        //Allows FaultContract(s) to callback
        [OperationContract(IsOneWay = false)]
        //Allows TransactionScope at client
        [TransactionFlow(TransactionFlowOption.Allowed)]
        //Allows SOAP faults to client
        [FaultContract(typeof(ApplicationException))]
        [FaultContract(typeof(ArgumentException))]
        [FaultContract(typeof(Exception))]
        Response ExecuteRequest(Request request);
    }
}

从上面的代码可以看出,有一个单独的 ExecuteRequest OperationContract,它接受一个 Request 并返回一个 Response。听起来很简单,但是会有很多很多的 Request 对象吗?嗯,实际上是的,但这只是标准 OO 多态性在起作用,我们可以将 Request 的任何子类存储在其基类中。我稍后会介绍这一点;现在,让我们继续检查服务。可以看出,单个 ExecuteRequest OperationContract 被标记为 OneWay=false;这意味着服务会回调客户端。您可能会注意到我没有在任何地方指定 CallBack 接口,那么 OneWay=false 到底是什么意思呢?很简单,它允许将故障发送回客户端。稍后会详细介绍。另一个需要注意的是,如果客户端希望使用事务,服务允许使用事务。

现在,我们来看看 Request 对象,好吗?

using System;
using System.Runtime.Serialization;

namespace Service.Portal
{
    /// <summary>
    /// A Base request
    /// </summary>
    [DataContract]
    [KnownType(typeof(CustomerRequest))]
    [KnownType(typeof(CustomerSearchRequest))]
    public abstract class Request : IExtensibleDataObject
    {
        #region Data
        private ExtensionDataObject extensionDataObject=null;
        #endregion

        #region Abstract Methods
        public abstract Response CreateResponse(Object requestResults);
        public abstract Object Execute();
        #endregion

        ....
        ....
    }

这基本上就是它的全部。通过使用这种方法,您不必担心服务变更,因为 ServiceContract 始终相同;唯一改变的是 Request 的数量。

这是一个特定的 Response DataContract

namespace Service.Portal
{
    [DataContract]
    public class CustomerResponse : Response
    {
        [DataMember(IsRequired = true, Name = "Customers")]
        public List<Customers> Customers { get; set; }

    }
}

已知类型 (KnownTypes)

WCF 的一个非常酷的功能是,您可以公开一个具有许多子类的类型,例如 Request,并且您可以使用 KnowTypeAttribute 标记基类(本例中为 Request),然后 DataContractSerializer 将知道如何处理这些类型。

在随附的演示应用程序中,Request 只有两个子类,但它们应该足以演示使用 KnowType 的概念。

IExtensibleDataObject

"IExtensibleDataObject 接口提供了一个属性,用于设置或返回一个结构,该结构用于存储数据契约之外的外部数据。额外的数据存储在 ExtensionDataObject 类的实例中,并通过 ExtensionData 属性访问。在数据接收、处理和发送回的往返操作中,额外的数据会完整地发送回原始发送方。这对于存储从未来版本的契约接收到的数据非常有用。如果您不实现该接口,则在往返操作期间,任何额外的数据都将被忽略和丢弃。"

MSDN 链接:http://msdn.microsoft.com/en-us/library/system.runtime.serialization.iextensibledataobject.aspx

实际上,这意味着通过让您的 DataContract 类实现 IExtensibleDataObject,您正在创建一个可版本化的对象。

在随附的演示代码中,Request/Response 实现了 IExtensibleDataObject,如下所示

using System;
using System.Runtime.Serialization;

namespace Service.Portal
{
    [DataContract]
    [KnownType(typeof(CustomerRequest))]
    [KnownType(typeof(CustomerSearchRequest))]
    public abstract class Request : IExtensibleDataObject
    {
        private ExtensionDataObject extensionDataObject=null;


        public ExtensionDataObject ExtensionData
        {
            get { return extensionDataObject; }
            set { extensionDataObject = value; }
        }
    }
}

ExtensionDataObject 具有对象引用和类型信息的内部链表,并且它知道未知数据成员的存储位置。当对象实现 IExtensibleDataObject 并且包含未知数据成员的请求发出时,它们会存储在内部链表中的未知成员列表中。如果请求包含未知类型的未知数据成员,则可以使用 ExtensionDataObject 的内部未知数据成员列表找到并反序列化这些未知数据成员。

基本上,这是一个好主意,我建议你们都这样做。

故障契约

现在我们将访问 WCF 服务,并查看实现 ServiceContract 的类。

WCF 服务实际上非常简单;整个 ServiceContract 如下所示

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

namespace Service.Portal
{
    [ServiceContract(
        Namespace = "http://www.sachabarber.net/2008/cp", 
        SessionMode = SessionMode.Allowed)]
    public interface IPortal
    {

        //Allows SOAP faults to client
        [FaultContract(typeof(ApplicationException))]
        [FaultContract(typeof(ArgumentException))]
        [FaultContract(typeof(Exception))]
        Response ExecuteRequest(Request request);
    }
}

我们可以看到,我们正在捕获 Exception 并抛出一些看起来很奇怪的 FaultException<T>,其中 FaultException<T> 用于客户端应用程序以捕获契约中指定的 SOAP 故障。可以看出,服务实现设置为按调用,这意味着每次调用都会获得一个新服务,并且我们无需处理线程同步上下文。另一个要提的是,我们有一个小助手,可以在调试模式下使用 IncludeExceptionDetailInFault,而在发布模式下则不使用。单一操作也标记为不允许模拟。在随附的服务代码中没有必要允许模拟,因为它是调用链中唯一的服务,因此无需将当前凭据流向另一个服务。我还坚持服务参与事务调用,其中客户端代码需要使用 TransactionScope 类来管理事务。TransactionAutoComplete 属性指示如果没有发生未处理的异常,则事务范围会自动完成。

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

namespace Service.Portal
{
    [ServiceBehavior(
        InstanceContextMode= InstanceContextMode.PerCall, 
        UseSynchronizationContext = false, 
        IncludeExceptionDetailInFaults =  
        DebugHelper.IncludeExceptionsInFaults)]
    public class Portal : IPortal
    {

        #region IPortal Members

        [OperationBehavior(Impersonation = ImpersonationOption.NotAllowed, 
            TransactionScopeRequired = true, TransactionAutoComplete = true)]
        public Response ExecuteRequest(Request request)
        {
            Response r = null;

            if (request == null)
                throw new ArgumentNullException("request");

            try
            {
                Object results = request.Execute();
                r = request.CreateResponse(results);
            }
              ...
            ...
            ...
            catch (Exception ex)
            {
                Console.WriteLine(String.Format("Server Exception {0}", 
                    ex.Message));

                throw new FaultException<Exception>(
                    new Exception(ex.Message));
            }


            return r;
        }

 

        #endregion

    }

    //simply debug class to IncludeExceptionsInFaults in debug mode
    public static class DebugHelper
    {
        public const bool IncludeExceptionsInFaults =
#if DEBUG
            true;
#else
        false;
#endif

    }
}

Windows 服务托管和安装

正如我在本文开头所说,WCF 服务实际上托管在 Windows 服务中。在随附的演示中,您会发现一个单独的项目,如果您处于调试模式(您将右键单击 PortalHost 项目并选择“调试”),则可以使用以下代码调试 WCF 服务,或者如果您处于发布模式,则正常运行服务。

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

namespace PortalHost
{
    class Program
    {
        static void Main(string[] args)
        {
#if (!DEBUG)
            try
            {
                ServiceBase[] ServicesToRun;
                ServicesToRun = new ServiceBase[] { new Service() };
                ServiceBase.Run(ServicesToRun);
            }
            catch (Exception e)
            {
                Console.WriteLine("Error occurred " + e.Message);
            }
#else
            try
            {
                Console.WriteLine("Starting Service");
                Service.StartService();
                Console.WriteLine("Service Started");
                System.Threading.Thread.Sleep(System.Threading.Timeout.Infinite);

            }
            catch (Exception e)
            {
                Console.WriteLine("Error occurred " + e.Message);
            }
#endif
        }
    }
}

实际的 Windows 服务类如下所示

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.ServiceProcess;
using System.Text;
using System.ServiceModel;
using System.Reflection;
using Service.Portal;
using System.ServiceModel.Description;

namespace PortalHost
{
    public partial class Service : ServiceBase
    {
        private static ServiceHost portalHost;

        public Service()
        {
            
        }

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

        public static void StartService()
        {
            try
            {
                //WCF service hosting
                portalHost = new ServiceHost(typeof(Portal));
                StartServiceHost(portalHost);
            }
            catch (TargetInvocationException tiEx)
            {
                Console.WriteLine("Error occurred " + tiEx.Message);
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error occurred " + ex.Message);
            }
        }

        ....
        ....

        private static void StartServiceHost(ServiceHost serviceHost)
        {
            try
            {
                // We will recreate the service host here to be sure we don't have a 
                //service in a faulted state
                serviceHost = new ServiceHost(serviceHost.Description.ServiceType);
                Console.WriteLine("Attempting to open Service.Portal service.");
                serviceHost.Open();
                serviceHost.Faulted += new EventHandler(ServiceHost_Faulted);
            }
              ....
              ....
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }
        }
        ....
        ....

    }
}

此外,PortalHost 项目还包含一个自定义安装程序,如下所示

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration.Install;
using System.Linq;
using System.ServiceProcess.Design;
using System.ServiceProcess;
using System.Windows.Forms;


namespace PortalHost
{
    [RunInstaller(true)]
    public partial class ProjectInstaller : Installer
    {
        public ProjectInstaller()
        {
            InitializeComponent();
        }

        // Prompt the user for service installation account values.
        public static bool GetServiceAccount(ref ServiceProcessInstaller svcInst)
        {
            bool accountSet = false;
            ServiceInstallerDialog svcDialog = new ServiceInstallerDialog();
            //Use the ServiceInstallerDialog to customise the username and password for
            //the installation process
            ....
            ....

            return accountSet;
        }

    }
}

我们正在使用以下支持类 ServiceProcessInstaller/ServiceInstaller 和一个 ServiceInstallerDialog(用于在安装期间捕获用户名和密码)。

处理 Windows 服务中出现故障的 WCF 通道

在处理 WCF 时,最不想听到的就是通道出现故障;这真是令人难以置信的坏消息。基本上,一个故障通道是完全无用的。在托管 Windows 服务中,我们能对这种情况做些什么呢?嗯,很简单,我们所做的是捕获 ServiceHost.Faulted 异常,WCF 允许我们这样做(谢天谢地),然后停止/重新启动 WCF 服务。这在下面演示

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.ServiceProcess;
using System.Text;
using System.ServiceModel;
using System.Reflection;
using Service.Portal;
using System.ServiceModel.Description;

namespace PortalHost
{
    public partial class Service : ServiceBase
    {
        private static ServiceHost portalHost;

        public Service()
        {
            
        }

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

        public static void StartService()
        {
            ....
            ....
        }

        private static void StartServiceHost(ServiceHost serviceHost)
        {
             ....
            ....
        }

        private static void StopServiceHost(ServiceHost serviceHost)
        {
            ....
            ....
        }

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


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

安装

如前所述,随附的演示 WCF 服务实际上托管在 Windows 服务中,并且我们大多数人都应该知道,Windows 服务在特定用户帐户下运行。但是如何安装 Windows 服务呢?

随附的演示代码包含一个安装程序,用于安装托管 WCF 服务的 Windows 服务。作为此安装过程的一部分,系统将提示您输入服务的登录凭据。

使用 InstallUtil.exe

Windows 服务可以使用 InstallUtil.exe 命令行实用程序进行安装。为了安装附带的 Portal 服务,您需要类似以下内容

installutil.exe C:\Users\sacha\Desktop\Linq2WCF_NEW\
                   Linq2WCF\Linq2WCF\PortalHost\bin\Release\PortalHost.exe

运行后会出现一个对话框,您可以在其中输入 Windows 服务使用的帐户详细信息。

使用 MSI

随附的演示解决方案实际上有一个名为“PortalMSIInstaller”的安装程序,这是一个典型的安装程序/设置项目,它只是使用 Windows 服务项目“PortalHost”的输出。使用此项目唯一要注意的是,服务已安装,然后必须配置为使用正确的登录凭据,因为这些凭据不是安装过程的一部分。MSI 还使用 ServiceInstallerDialog,但此窗口有时会被推到后面,因此您可能需要在安装过程中使用任务管理器来查找它。

服务自定义安装程序中使用的 ServiceInstallerDialog 代码,用于显示以下服务登录屏幕,如下所示

// Prompt the user for service installation account values.
public static bool GetServiceAccount(ref ServiceProcessInstaller svcInst)
{
    bool accountSet = false;
    ServiceInstallerDialog svcDialog = new ServiceInstallerDialog();

    // Query the user for the service account type.
    do
    {
        svcDialog.TopMost = true;
        svcDialog.StartPosition = FormStartPosition.CenterScreen;
        svcDialog.ShowDialog();

        if (svcDialog.Result == ServiceInstallerDialogResult.OK)
        {
            // Do a very simple validation on the user
            // input. Check to see whether the user name
            // or password is blank.

            if ((svcDialog.Username.Length > 0) &&
                (svcDialog.Password.Length > 0))
            {
                // Use the account and password.
                accountSet = true;

                svcInst.Account = ServiceAccount.User;
                svcInst.Username = svcDialog.Username;
                svcInst.Password = svcDialog.Password;
            }
        }
        else if (svcDialog.Result == ServiceInstallerDialogResult.UseSystem)
        {
            svcInst.Account = ServiceAccount.LocalSystem;
            svcInst.Username = null;
            svcInst.Password = null;
            accountSet = true;
        }

        if (!accountSet)
        {
            // Display a message box. Tell the user to
            // enter a valid user and password, or cancel
            // out to leave the service account alone.
            DialogResult result;
            result = MessageBox.Show(
                "Invalid user name or password for service installation." +
                "  Press Cancel to leave the service account unchanged.",
                "Change Service Account",
                MessageBoxButtons.OKCancel,
                MessageBoxIcon.Hand);

            if (result == DialogResult.Cancel)
            {
                // Break out of loop.
                break;
            }
        }
    } while (!accountSet);

    return accountSet;
}

安装程序运行后,它将允许您使用以下对话框输入服务登录凭据,以便在其下运行

安装程序运行后,您需要更改配置文件以指向您自己的 SQL Server 安装。默认配置文件是安装程序项目使用的输出项目的名称,配置文件将类似于“PortalHost.exe.config”,您应该确保更改 <connectionStrings> 部分。

我将把查看“PortalMSIInstaller”项目作为读者的练习;这都是相当标准的东西。

启动 Windows 服务(托管 WCF 服务)

服务安装后,您需要启动它。这可以使用服务管理器轻松完成。

WPF 客户端

随附的“WPFClient”项目执行以下功能

  • 提供 WCF 设置诊断屏幕,这在工作中非常宝贵,因为我们可以让非技术用户打印其设置屏幕,而无需查找和打开 App.Config 文件。
  • 提供对 LINQ to Entities 检索到的客户对象列表的 3D 轮播视图。
  • 提供当前客户的 LINQ to Entities 检索到的订单对象列表。
  • 提供一个搜索构建器屏幕,允许用户构建动态查询,以便通过 WCF 请求查询 LINQ to Entities 层。

我将在下面其余部分依次处理这些领域。

WPFClient 项目基本设置

在我们开始深入代码之前,我想谈谈应用程序的大致结构。

Windows

有两个窗口:一个诊断窗口和一个主窗口。主窗口托管一个 3D 旋转木马以及 CurrentCustomerControlCurrentCustomerOrdersControl 控件。

用户控件

  • CurrentCustomerControl:一个非常简单的用户控件,显示反射图像和当前选定的 Northwind.Customers 名称。
  • CurrentCustomerOrdersControl:在 ItemsControl 中显示当前选定的 Northwind.Customers.Orders 列表。有三种不同的视图可以向用户显示更多或更少关于绑定数据的信息。
  • SearchControl:包含 n 个 SearchClauseControl
  • SearchClauseControl:允许为绑定对象的单个属性构建搜索子句。

ViewModels

随附代码对主窗口 (CustomerOrdersViewModel) 和 SearchClauseControl (SearchClauseViewModel) 使用 MVVM 模式。

代码库的其余部分

至于代码的其余部分,我认为它们都是相当标准的 WPF 内容,例如附加属性/样式/命令等。

确保为 WCF 服务调用提供了正确的用户

为了正确使用托管在 Windows 服务中的 WCF 服务(假设它已安装并正在运行),您需要提供登录凭据;这是通过随附代码中“WPFClient”项目的 App.Config 文件中的 identity\userPrincipalName 部分完成的。

<identity>
  <userPrincipalName value="YOUR PC\YOUR USER" />
</identity>

WCF 设置诊断

我认为,任何能帮助最终用户解决他们可能遇到的问题的帮助都是一件好事。

还应该记住,有些用户可能实际上没有技术头脑,所以他们不会喜欢费力寻找 App.Config 文件并查看 XML;我敢说有些人甚至不知道 XML 是什么。所以在我看来,保护用户免受此困扰是非常必要的。

为此,我创建了一个小的诊断屏幕,允许用户查看与给定客户端 App.Config 文件关联的 WCF 配置的诊断信息。以下代码同样适用于 WPF/WinForms 或使用 WCF 服务的控制台应用程序。

该代码主要使用 System.ServiceModel.Configuration 命名空间中找到的一些基于配置的类,以及一点点反射来搜索 App.Config 文件中的所有相关 WCF 元素,并将其用于在诊断窗口中显示。

主要代码如下所示

/// <summary>
/// Gets client EndPoint information out of the attached App.Config
/// </summary>
public List<String> EndPoints
{
    get
    {
        // Automatically find all client endpoints and related 
        //bindings defined in app.config
        List<String> endpointNames = new List<String>();

        try
        {
            BindingsSection bindingsSection =
                ConfigurationManager.GetSection(
                "system.serviceModel/bindings") as BindingsSection;

            if (bindingsSection == null)
            {
                Console.WriteLine(
                    "The configuration file doesn't contain " +
                    "a system.serviceModel/bindings configuration section");
            }
            else
            {
                endpointNames.Add("BINDINGS");
                foreach (BindingCollectionElement bce in 
                    bindingsSection.BindingCollections)
                {
                    AnalyseBinding(endpointNames, bce);
                }
            }

            ClientSection clientSection =
                ConfigurationManager.GetSection(
                "system.serviceModel/client") as ClientSection;

            if (clientSection == null)
            {
                Console.WriteLine("The configuration file doesn't " + 
                    "contain a system.serviceModel/client configuration section");
            }
            else
            {
                ChannelEndpointElementCollection endpointCollection =
                    clientSection.ElementInformation.
                    Properties[String.Empty].Value as
                    ChannelEndpointElementCollection;

                endpointNames.Add(Environment.NewLine);
                endpointNames.Add("ENDPOINTS");
                foreach (ChannelEndpointElement 
                    endpointElement in endpointCollection)
                {
                    GetPropetiesFromType(endpointNames,
                        endpointElement.GetType(), endpointElement);
                }                        
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);

        }
        return endpointNames;
    }
}

这里有两个用于获取属性值的小辅助方法

private void AnalyseBinding(List<string> endpointNames, 
             BindingCollectionElement bce)
{
    foreach (IBindingConfigurationElement be in bce.ConfiguredBindings)
    {
        GetPropetiesFromType(endpointNames, be.GetType(), be);
    }
}

private void GetPropetiesFromType(List<string> endpointNames, 
                                  Type type, Object source)
{
    PropertyInfo[] pis = type.GetProperties();
    if (pis.Length > 0)
    {
        foreach (PropertyInfo pi in pis)
        {
            bool foundSysName = false;
            String currentValue = pi.GetValue(source, null).ToString();
            if (currentValue.Contains("System.Configuration"))
                return;

            if (currentValue.StartsWith("System.ServiceModel"))
            {
                Object o = pi.GetValue(source, null);
                foundSysName = true;
                GetPropetiesFromType(endpointNames, o.GetType(), o);

            }
            if (!foundSysName)
                endpointNames.Add(String.Format("{0} - {1}",
                    pi.Name, currentValue));
            else
                endpointNames.Add(String.Format("{0} - {1}",
                    pi.Name, "<SEE ABOVE>"));
        }
        endpointNames.Add(Environment.NewLine);
    }
}

3D 旋转木马

对于 3D 轮播,我使用的是 Pavan Podila 的 FluidKit CodePlex 贡献中的真正出色的 ElementFlow。这是一个我过去曾尝试自己创建的优秀 WPF 控件。当我发现 Pavan 的代码时,我基本上放弃了,因为它太酷了。它就像一个普通的面板一样工作,这意味着你可以在 ListBoxItemsControl 中使用它,而不是这些控件使用的普通面板。Pavan 做得非常出色,这个 ElementFlow 具有以下功能

  • SelectedItem
  • 反射开|关
  • 标准控件中包含许多不同的视图,您可以使用 F12 键查看其运行情况。我只使用以下视图,但还有其他视图
    • CoverFlow
    • 旋转木马
    • 过山车
    • 旋转名片夹

3D 旋转木马最初加载了 15 个 Northwind.Customers 的列表,其中客户必须有两个以上的订单。这是通过 WCF CustomerRequest 完成的,在应用程序初始启动时执行。

客户订单列表

从用于保存包含订单的 Northwind.Customers 列表的 3D 旋转木马中,选定的 Customer 用于绑定到 CurrentCustomerOrdersControl 控件中的 DP。并且在 CurrentCustomerOrdersControl 控件内部,CustomerOrders DP 设置为当前的 Northwind.Customers.Orders。这在下面显示

/// <summary>
/// CustomerOrders Dependency Property
/// </summary>
public static readonly DependencyProperty CustomerOrdersProperty =
    DependencyProperty.Register("CustomerOrders", typeof(List<Orders>), 
    typeof(CurrentCustomerOrdersControl),
        new FrameworkPropertyMetadata(null,
            new PropertyChangedCallback(OnCustomerOrdersChanged)));

/// <summary>
/// Gets or sets the CustomerOrders property.
/// </summary>
public List<Orders> CustomerOrders
{
    get { return (List<Orders>)GetValue(CustomerOrdersProperty); }
    set { SetValue(CustomerOrdersProperty, value); }
}

/// <summary>
/// Handles changes to the CustomerOrders property.
/// </summary>
private static void OnCustomerOrdersChanged(DependencyObject d,
    DependencyPropertyChangedEventArgs e)
{
    List<Orders> orders = e.NewValue as List<Orders>;

    if (orders != null)
        ((CurrentCustomerOrdersControl)d).lstOrders.ItemsSource = orders;
}

/// <summary>
/// BoundCustomer Dependency Property
/// </summary>
public static readonly DependencyProperty BoundCustomerProperty =
    DependencyProperty.Register("BoundCustomer", typeof(Customers), 
        typeof(CurrentCustomerOrdersControl),
        new FrameworkPropertyMetadata(null,
            new PropertyChangedCallback(OnBoundCustomerChanged)));

/// <summary>
/// Gets or sets the BoundCustomer property.
/// </summary>
public Customers BoundCustomer
{
    get { return (Customers)GetValue(BoundCustomerProperty); }
    set { SetValue(BoundCustomerProperty, value); }
}

/// <summary>
/// Handles changes to the BoundCustomer property.
/// </summary>
private static void OnBoundCustomerChanged(DependencyObject d, 
    DependencyPropertyChangedEventArgs e)
{
    Customers customer = e.NewValue as Customers;

    if (customer !=null)
        ((CurrentCustomerOrdersControl) d).CustomerOrders = customer.Orders;
}

一旦获取到 Northwind.Customers.Orders,它们将用作 CurrentCustomerOrdersControl 中嵌入的 ItemsControl 的源。

CurrentCustomerOrdersControl 上还有三个按钮,可用于更改应用于 ItemsControl 的当前 DataTemplate

搜索构建器

在本文的开头,我声明随附的代码使用 LINQ to Entities 框架,并且我们将在 UI 中编写动态查询,然后将其发送到 WCF 服务并用于查询 LINQ to Entities 框架。

这通过两个用户控件在 UI 中完成。

SearchClauseControl

这是一个简单的控件,它接受一个 Type,然后使用反射来构建各种属性/允许值,用户可以根据当前选定的 Types 属性类型进行选择。与此控件关联的大部分逻辑都是通过 SearchClauseViewModel 完成的。SearchClauseControl 也由父级 SearchControl 调用,以便将当前搜索子句查询添加到任何其他搜索子句查询中。

SearchClauseControl 单一提供实际查询字符串的属性如下所示

public String ClauseResult
{
    get
    {
        StringBuilder sb = new StringBuilder(1000);

        if (!IsFirst)
            sb.Append(String.Format("{0} ", currentOperator));

        if (IsCollectionProperty)
            sb.Append(String.Format("{0}.Count ", currentProperty.Name));
        else
        {
            if (IsString)
                sb.Append(String.Format("{0}.", currentProperty.Name));
            else
                sb.Append(String.Format("{0} ", currentProperty.Name));
        }

        if (IsOperatorComparable)
        {
            sb.Append(String.Format("{0} ", currentCondition));
            sb.Append(String.Format("@{0} ", ParameterNumber));
        }

        if (IsString)
        {
            sb.Append(String.Format("{0}(@{1})",
                currentCondition,
                ParameterNumber));
        }

        return sb.ToString();
    }
}

SearchControl

基本上是一个容器,它托管了许多 SearchClauseControl 控件。SearchControl 唯一做的就是根据其所有包含的 SearchClauseControl 控件的当前配置构建一个动态查询字符串。当创建动态查询字符串时,它(通过 SearchRequestedEvent 路由事件)发送到主窗口,主窗口反过来创建一个 CustomerSearchRequest 查询并运行该查询。

当主窗口 (CustomOrdersWindow) 中看到 SearchControl.SearchRequested 路由事件时,实际发生的是 SearchControl.SearchRequested 事件被订阅,然后调用主窗口的视图模型 (CustomerOrdersViewModel) 的 SearchCustomerWithOrders() 方法。

此方法如下所示

public void SearchCustomerWithOrders(String whereClause, List<Object> searchParameters)
{
    try
    {
        Service<IPortal>.Use((client) =>
        {
            using (TransactionScope scope = new TransactionScope())
            {
                CustomerSearchRequest request = new CustomerSearchRequest();
                request.WhereClause = whereClause;
                request.SearchParameters = searchParameters;

                CustomerResponse response =
                    (CustomerResponse)client.ExecuteRequest(request);
                if (response.Customers != null)
                    CurrentCustomers =
                        new ObservableCollection<Customers>(response.Customers);

                if (CurrentCustomers.Count > 0)
                    SelectedCustomer = CurrentCustomers[0];

                //complete the Transaction
                scope.Complete();
            }
        });
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
}

这里实际发生的是我们正在使用 Service<T> 类的静态方法,这确实是一个非常酷的类。让我们看看它。

using System.ServiceModel;
using System;

namespace WpfClient.ServiceProxy
{
    /// Service client delegate
    public delegate void UseServiceDelegate<T>(T proxy); 

    /// <summary>
    /// A helper class to run code using a WCF client proxy
    /// 
    /// This weird class came from 
    /// http://www.iserviceoriented.com/blog/post/Indisposable+-+WCF+Gotcha+1.aspx
    /// </summary>
    public static class Service<T>
    {
        public static ChannelFactory<T> _channelFactory = 
            new ChannelFactory<T>("PortalServiceTcp");

        /// <summary>
        /// Creates the WCF service proxy and runs the codeBlock delegate
        /// using the WCF service proxy that is created
        /// </summary>
        /// <param name="codeBlock">The code to run using the WCF Proxy</param>
        public static void Use(UseServiceDelegate<T> codeBlock)
        {
            IClientChannel proxy = (IClientChannel)_channelFactory.CreateChannel(); 
            bool success = false; 
            try 
            { 
                codeBlock((T)proxy); 
                proxy.Close(); 
                success = true; 
            }
            catch (FaultException<ApplicationException> dex)
            {
                Console.WriteLine("Client FaultException<ApplicationException>");
            }
            catch (FaultException<ArgumentException> aex)
            {
                Console.WriteLine("Client FaultException<ArgumentException>");
            }
            catch (FaultException<Exception> ex)
            {
                Console.WriteLine("Client FaultException<Exception>");
            }
            catch (FaultException fEx)
            {
                Console.WriteLine("Client FaultException");
            }
            finally
            {
                if (!success)
                {
                    proxy.Abort();
                }
            }
        }
    }
}

可以看出,该方法具有以下签名:Service<T>.Use(UseServiceDelegate<T> codeBlock);所以本质上发生的是创建了一个新的 WCF 代理,然后调用传递给 Service<T>.Use 方法的委托,其中新创建的 WCF 代理类作为参数传递给原始委托(来自前面显示的 CustomerOrdersViewModel.SearchCustomerWithOrders 代码中的 lambda 表达式 (client) => { ...})。

很简洁,不是吗?这就是附带的 WPFClient 代码中所有对 WCF 服务的调用方式。

所以现在我们有了一个实际的 WCF 代理,CustomerOrdersViewModel.SearchCustomerWithOrders() 中的 lambda 被运行,它创建了一个新的 CustomerSearchRequest,然后将其发送到 WCF 服务。让我们检查 CustomerSearchRequest,看看它是如何工作的。

using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Linq.Expressions;
using System.Linq;

//Linq2Entities
using LINQ2Entities;
using System.Data.Objects;
using System.Linq.Dynamic;

namespace Service.Portal
{
    [DataContract]
    public class CustomerSearchRequest : Request
    {
        [DataMember]
        public List<Object> SearchParameters { get; set; }

        [DataMember]
        public String WhereClause { get; set; }

        public override Response CreateResponse(Object requestResults)
        {
            CustomerResponse response = new CustomerResponse();
            response.Customers = requestResults as List<Customers>;
            return response;
        }

        public override Object Execute()
        {
            NorthwindEntities context = new NorthwindEntities();

            List<Customers> custs =
                context.Customers.Include("Orders")
                .Where(this.WhereClause, 
                this.SearchParameters.ToArray()).ToList();

            return custs;
        }
        #endregion
    }
}

多亏了 LINQ to Entities 部分中讨论的 DynamicQuery API 提供的漂亮扩展方法,进行动态查询几乎变得微不足道;我们只需包含一个 Where 子句,并使用在 WPF 客户端中创建的查询字符串以及从 WPF 客户端传递到 WCF 调用的相关参数值。

就是这样

这就是我想说的全部内容,如果您喜欢这篇文章,请投票并发表评论,谢谢。

特别感谢

我只想感谢我现在工作的出色团队,他们让我学到了更多关于 WCF 的知识,并为我提供了某些 WCF 问题的答案。谢谢大家,你们知道我是谁。

参考文献

© . All rights reserved.