譯文,個(gè)人原創(chuàng),轉(zhuǎn)載請注明出處(C# 6 與 .NET Core 1.0 高級編程 - 39 章 Windows 服務(wù)(上)),不對的地方歡迎指出與交流。  

章節(jié)出自《Professional C# 6 and .NET Core 1.0》。水平有限,各位閱讀時(shí)仔細(xì)分辨,唯望莫誤人子弟。 

附英文版原文:Professional C# 6 and .NET Core 1.0 - Chapter 39 Windows Services

----------------------------------------------------- 

本章主要內(nèi)容

  • Windows服務(wù)的體系結(jié)構(gòu)

  • 創(chuàng)建Windows服務(wù)程序

  • Windows服務(wù)安裝程序

  • Windows服務(wù)控制程序

  • 疑難解答Windows服務(wù)

Wrox.com網(wǎng)站中本章源代碼下載

本章的wrox.com代碼下載位于 www.wrox.com/go/professionalcsharp6 下載代碼選項(xiàng)卡。代碼在"Chapter 39",以下名稱的項(xiàng)目貫穿整個(gè)章節(jié)。

  • Quote Server

  • Quote Client

  • Quote Service

  • Service Control

什么是Windows服務(wù)?

Windows服務(wù)是可以在開機(jī)時(shí)自動(dòng)啟動(dòng)的程序,而無須任何人登錄到計(jì)算機(jī)。如果需要在沒有用戶交互的情況下啟動(dòng)程序,或者在不是交互式用戶的用戶下運(yùn)行程序 - 這種用戶可能需要更多的權(quán)限,則可以創(chuàng)建Windows服務(wù)。一些示例可能是WCF 主宿程序(如果由于某種原因不能使用Internet信息服務(wù)(IIS)),這種程序可以從網(wǎng)絡(luò)服務(wù)器獲取緩存數(shù)據(jù),或者在后臺(tái)重新組織本地磁盤數(shù)據(jù)。

本章從查看Windows服務(wù)的架構(gòu)開始,創(chuàng)建托管網(wǎng)絡(luò)服務(wù)器的Windows服務(wù),并提供有關(guān)啟動(dòng)、監(jiān)視、控制和解決Windows服務(wù)故障的信息。

如上所述,Windows服務(wù)是可以在操作系統(tǒng)引導(dǎo)時(shí)自動(dòng)啟動(dòng)的應(yīng)用程序。這些應(yīng)用程序可以在沒有交互式用戶登錄到系統(tǒng)的情況下運(yùn)行,并且可以在后臺(tái)進(jìn)行一些處理。

例如,在Windows Server上,應(yīng)該可以從客戶端訪問系統(tǒng)網(wǎng)絡(luò)服務(wù),而無需用戶登錄到服務(wù)器;在客戶端系統(tǒng)上,Windows服務(wù)使您能夠執(zhí)行諸如獲取在線新軟件版本或在本地磁盤上清除文件等操作。

可以將Windows服務(wù)配置為從特殊配置的用戶帳戶或系統(tǒng)用戶帳戶下運(yùn)行 - 該用戶帳戶需具有比系統(tǒng)管理員更多的特權(quán)。

注意 除非另有說明,提到服務(wù)時(shí),指的是Windows服務(wù)。

以下是幾個(gè)Windows服務(wù)的例子:

  • Simple TCP/IP服務(wù)是一種承載一些小型TCP/IP服務(wù)器的服務(wù)程序:echo,daytime,quote及其他。

  • 萬維網(wǎng)發(fā)布服務(wù)是 IIS的一種服務(wù)。

  • 事件日志是將消息記錄到事件日志系統(tǒng)的服務(wù)。

  • Windows搜索是一種在磁盤上創(chuàng)建數(shù)據(jù)索引的服務(wù)。

  • 超級預(yù)取是將常用的應(yīng)用程序和庫預(yù)裝載到內(nèi)存中的服務(wù),從而提高這些應(yīng)用程序的啟動(dòng)時(shí)間。

可以使用服務(wù)管理工具(如圖39.1所示)查看系統(tǒng)上的所有服務(wù)。通過在“開始”菜單輸入“Services”(譯者注:如果Services無響應(yīng),可嘗試輸入"services.msc")訪問該程序。

photoshop培訓(xùn),電腦培訓(xùn),電腦維修培訓(xùn),移動(dòng)軟件開發(fā)培訓(xùn),網(wǎng)站設(shè)計(jì)培訓(xùn),網(wǎng)站建設(shè)培訓(xùn)

圖39.1

注意 不能使用.NET Core創(chuàng)建Windows服務(wù),必須要.NET Framework才可以。但控制Windows服務(wù)可以使用.NET Core。

Windows服務(wù)架構(gòu)

操作Windows服務(wù)需要三種類型的程序:

  • 服務(wù)程序

  • 服務(wù)控制程序

  • 服務(wù)配置程序

服務(wù)程序是服務(wù)的實(shí)現(xiàn)。利用服務(wù)控制程序,可以向服務(wù)發(fā)送控制請求,例如開始、停止、暫停和繼續(xù)。通過服務(wù)配置程序,可以安裝服務(wù):把服務(wù)程序復(fù)制到文件系統(tǒng),同時(shí)將有關(guān)服務(wù)的信息寫入注冊表。此注冊表信息由服務(wù)控制管理器(SCM)用于啟動(dòng)和停止服務(wù)。.NET組件可以簡單地使用xcopy安裝,那是因?yàn)樗鼈儾恍枰獙⑿畔懭胱员?- 但安裝服務(wù)需要配置注冊表。也可以使用服務(wù)配置程序稍后更改該服務(wù)的配置。 Windows服務(wù)的三個(gè)組成部分將在以下小節(jié)中討論。

服務(wù)程序

為了大體了解 .NET實(shí)現(xiàn)的服務(wù),本節(jié)從總體上簡要介紹服務(wù)的Windows體系結(jié)構(gòu)以及服務(wù)的內(nèi)部功能。

服務(wù)程序?qū)崿F(xiàn)服務(wù)的功能需要三個(gè)部分:

  • 主函數(shù)

  • 主服務(wù)函數(shù)

  • 處理事件

在討論這些部分之前,有必要暫時(shí)岔開主題去簡單介紹SCM,它在向服務(wù)發(fā)送啟動(dòng)和停止的請求中起了重要作用。

服務(wù)控制管理器

SCM是操作系統(tǒng)中服務(wù)通信的一部分。序列圖39.2說明了通信的工作原理。

photoshop培訓(xùn),電腦培訓(xùn),電腦維修培訓(xùn),移動(dòng)軟件開發(fā)培訓(xùn),網(wǎng)站設(shè)計(jì)培訓(xùn),網(wǎng)站建設(shè)培訓(xùn)

 

圖39.2

開機(jī)時(shí)會(huì)啟動(dòng)所有設(shè)置為自動(dòng)啟動(dòng)服務(wù)的進(jìn)程,因此該進(jìn)程的主函數(shù)會(huì)被調(diào)用。Windows服務(wù)負(fù)責(zé)為其每個(gè)服務(wù)注冊主服務(wù)函數(shù)。主函數(shù)是服務(wù)程序的入口點(diǎn),在該功能中,主服務(wù)函數(shù)的入口點(diǎn)service-main在SCM中注冊。

主函數(shù),主服務(wù)和處理事件

服務(wù)的主函數(shù)Main方法是程序的普遍入口點(diǎn)。服務(wù)的主函數(shù)可能注冊多個(gè)主服務(wù)函數(shù)。 service-main函數(shù)包含服務(wù)的實(shí)際功能,必須為提供的每個(gè)服務(wù)注冊一個(gè)service-main函數(shù)。服務(wù)程序可以在單個(gè)程序中提供大量服務(wù);例如,<windows>\system32\services.exe是包括 Alerter,應(yīng)用程序管理,計(jì)算機(jī)瀏覽器和DHCP客戶端等服務(wù)程序。
SCM為每個(gè)要啟動(dòng)的服務(wù)調(diào)用service-main函數(shù)。service-main函數(shù)的一個(gè)重要任務(wù)是向SCM注冊處理事件。
處理事件是服務(wù)程序的第三部分。處理事件必須響應(yīng)來自SCM的事件。服務(wù)可以是停止,掛起和恢復(fù)事件,但處理事件必須對這些事件做出反應(yīng)。

SCM注冊處理事件之后,服務(wù)控制程序可以向SCM發(fā)布請求去停止,暫停和恢復(fù)服務(wù)。服務(wù)控制程序獨(dú)立于SCM和服務(wù)本身。操作系統(tǒng)包含許多服務(wù)控制程序,例如圖39.1中所示的Microsoft管理控制臺(tái)(MMC)服務(wù)管理單元。也可以編寫自己的服務(wù)控制程序,一個(gè)很好的例子是圖39.3中所示的SQL Server配置管理器,它在MMC下運(yùn)行。
photoshop培訓(xùn),電腦培訓(xùn),電腦維修培訓(xùn),移動(dòng)軟件開發(fā)培訓(xùn),網(wǎng)站設(shè)計(jì)培訓(xùn),網(wǎng)站建設(shè)培訓(xùn)

圖39.3

服務(wù)控制程序

顧名思義,通過服務(wù)控制程序,可以停止,掛起和恢復(fù)服務(wù)。通過服務(wù)控制程序可以向服務(wù)發(fā)送控制代碼,處理事件會(huì)對這些事件做出反應(yīng)。還可以向服務(wù)詢問其實(shí)際狀態(tài)(如果服務(wù)正在運(yùn)行或暫停,或處于某種故障狀態(tài)),并實(shí)現(xiàn)響應(yīng)自定義控制代碼的自定義處理事件。

服務(wù)配置程序

由于必須在注冊表中配置服務(wù),因此不能用xcopy去安裝服務(wù)。注冊表包含服務(wù)的啟動(dòng)類型,啟動(dòng)類型可以設(shè)置為自動(dòng)、手動(dòng)或禁用。還需要配置服務(wù)程序的用戶和服務(wù)的依賴項(xiàng),例如,在當(dāng)前服務(wù)啟動(dòng)之前必須全部啟動(dòng)的服務(wù)。所有這些配置都在服務(wù)配置程序中完成。安裝程序可以使用服務(wù)配置程序來配置服務(wù),同時(shí)該程序也可以稍后用于更改服務(wù)配置參數(shù)。

Windows服務(wù)類

在.NET Framework中,可以在System.ServiceProcess命名空間中找到實(shí)現(xiàn)服務(wù)的三個(gè)服務(wù)類:

  • 必須繼承ServiceBase類才能實(shí)現(xiàn)服務(wù)。 ServiceBase類用于注冊服務(wù)和回應(yīng)啟動(dòng)和停止的請求。

  • ServiceController類用于實(shí)現(xiàn)服務(wù)控制程序。使用該類可以向服務(wù)發(fā)送請求。

  • ServiceProcessInstaller和ServiceInstaller類,顧名思義,是用來安裝和配置服務(wù)程序的類。

至此已準(zhǔn)備好去創(chuàng)建一個(gè)新的服務(wù)了。

創(chuàng)建Windows服務(wù)程序

本章中創(chuàng)建的服務(wù)托管于引用服務(wù)器(引用服務(wù)器 原文是 quote server)。隨著從客戶端發(fā)出的每個(gè)請求,引用服務(wù)器從引用文件返回隨機(jī)引用。解決方案的第一部分使用三個(gè)程序集:一個(gè)用于客戶端,兩個(gè)用于服務(wù)器。圖39.4提供了解決方案的概覽。QuoteServert程序集保存實(shí)際的功能。該服務(wù)讀取內(nèi)存緩存中的引用文件,并在套接字服務(wù)器的幫助下解答引用請求。 QuoteClient是一個(gè)WPF富客戶端應(yīng)用程序。此應(yīng)用程序創(chuàng)建一個(gè)客戶端套接字與QuoteServer通信。第三個(gè)程序集是實(shí)際的服務(wù), QuoteService啟動(dòng)和停止QuoteServer,即控制服務(wù)器。
photoshop培訓(xùn),電腦培訓(xùn),電腦維修培訓(xùn),移動(dòng)軟件開發(fā)培訓(xùn),網(wǎng)站設(shè)計(jì)培訓(xùn),網(wǎng)站建設(shè)培訓(xùn)

 圖39.4

創(chuàng)建程序的服務(wù)部分之前,在一個(gè)額外的C#類庫中創(chuàng)建一個(gè)簡單的套接字服務(wù)器,該庫將在服務(wù)過程中使用。接下來將會(huì)討論如何做到這一點(diǎn)。

為服務(wù)創(chuàng)建核心功能

在Windows服務(wù)中可以創(chuàng)建任何功能,例如掃描文件以執(zhí)行備份或檢查病毒或啟動(dòng)WCF服務(wù)器。但是,所有服務(wù)程序都有一些相似之處。程序必須能夠啟動(dòng)(并返回調(diào)用句柄)、停止和掛起。本節(jié)使用socket server 來查看這種實(shí)現(xiàn)。

Windows 10中Simple TCP/IP服務(wù)可以作為Windows組件的其中一部分去安裝。Simple TCP/IP服務(wù)的一部分是“一天的引用”或當(dāng)天引用(當(dāng)天引用 原文 qotd , 網(wǎng)上有解釋為 quotation of the day),TCP/IP服務(wù)器。這個(gè)簡單的服務(wù)偵聽端口17,并用來自文件<windows>\system32\drivers\etc\quotes的隨機(jī)消息來回答每個(gè)請求。示例服務(wù)將創(chuàng)建類似的服務(wù)器。示例服務(wù)器返回一個(gè)Unicode字符串,在qotd服務(wù)器則相反,它返回一個(gè)ASCII字符串。
首先,創(chuàng)建一個(gè)名為QuoteServer的類庫,并實(shí)現(xiàn)服務(wù)器的代碼。以下在源代碼文件QuoteServer.cs中的QuoteServer類:(代碼文件QuoteServer/QuoteServer.cs):

photoshop培訓(xùn),電腦培訓(xùn),電腦維修培訓(xùn),移動(dòng)軟件開發(fā)培訓(xùn),網(wǎng)站設(shè)計(jì)培訓(xùn),網(wǎng)站建設(shè)培訓(xùn)

using System;using System.Collections.Generic;using System.Diagnostics;using System.IO;using System.Linq;using System.Net;using System.Net.Sockets;using System.Text;using System.Threading.Tasks;namespace Wrox.ProCSharp.WinServices
{  public class QuoteServer
  {    private TcpListener _listener;    private int _port;    private string _filename;    private List<string> _quotes;    private Random _random;    private Task _listenerTask;

photoshop培訓(xùn),電腦培訓(xùn),電腦維修培訓(xùn),移動(dòng)軟件開發(fā)培訓(xùn),網(wǎng)站設(shè)計(jì)培訓(xùn),網(wǎng)站建設(shè)培訓(xùn)

構(gòu)造函數(shù)QuoteServer被重載以便文件名和端口可以傳遞調(diào)用。只傳遞文件名的構(gòu)造函數(shù)使用服務(wù)器的默認(rèn)端口7890。默認(rèn)構(gòu)造函數(shù)將引用的文件名默認(rèn)定義為quotes.txt:

photoshop培訓(xùn),電腦培訓(xùn),電腦維修培訓(xùn),移動(dòng)軟件開發(fā)培訓(xùn),網(wǎng)站設(shè)計(jì)培訓(xùn),網(wǎng)站建設(shè)培訓(xùn)

  public QuoteServer()
       : this ("quotes.txt")
    {
    }    public QuoteServer(string filename)
       : this (filename, 7890)
    {
    }    public QuoteServer(string filename, int port)
    {      if (filename == null) throw new  ArgumentNullException(nameof(filename));      if (port < IPEndPoint.MinPort || port > IPEndPoint.MaxPort)        throw new ArgumentException("port not valid", nameof(port));
      _filename = filename;
      _port = port;
    }

photoshop培訓(xùn),電腦培訓(xùn),電腦維修培訓(xùn),移動(dòng)軟件開發(fā)培訓(xùn),網(wǎng)站設(shè)計(jì)培訓(xùn),網(wǎng)站建設(shè)培訓(xùn)

ReadQuotes是一個(gè)幫助方法,它從構(gòu)造函數(shù)指定的文件中讀取所有引用。所有引用都添加到List <string>quotes 中。此外創(chuàng)建一個(gè)將用于返回隨機(jī)引用的Random類的實(shí)例:

photoshop培訓(xùn),電腦培訓(xùn),電腦維修培訓(xùn),移動(dòng)軟件開發(fā)培訓(xùn),網(wǎng)站設(shè)計(jì)培訓(xùn),網(wǎng)站建設(shè)培訓(xùn)

  protected void ReadQuotes()
    {      try
      {
        _quotes = File.ReadAllLines(filename).ToList();        if (_quotes.Count == 0)
        {          throw new QuoteException("quotes file is empty");
        }
        _random = new Random();
      }      catch (IOException ex)
      {        throw new QuoteException("I/O Error", ex);
      }
    }

photoshop培訓(xùn),電腦培訓(xùn),電腦維修培訓(xùn),移動(dòng)軟件開發(fā)培訓(xùn),網(wǎng)站設(shè)計(jì)培訓(xùn),網(wǎng)站建設(shè)培訓(xùn)

另一個(gè)幫助方法是GetRandomQuoteOfTheDay。此方法從引用集合返回一個(gè)隨機(jī)引用:

    protected string GetRandomQuoteOfTheDay()
    {      int index = random.Next(0, _quotes.Count);      return _quotes[index];
    }

在Start方法中,使用輔助方法ReadQuotes在List<string> quotes 中讀取包含引用的完整文件。此后,將啟動(dòng)一個(gè)新線程,它立即調(diào)用Listener方法 - 類似于第25章“網(wǎng)絡(luò)”中的TcpReceive示例。

這里使用任務(wù)是因?yàn)镾tart方法不能阻塞和等待客戶端,它必須立即返回到調(diào)用句柄(SCM)。如果方法沒有及時(shí)返回到調(diào)用句柄(30秒),則SCM認(rèn)為啟動(dòng)失敗。監(jiān)聽器任務(wù)是一個(gè)長期運(yùn)行的后臺(tái)線程。應(yīng)用程序可以退出而不停止此線程:

    public void Start()
    {
      ReadQuotes();
      _listenerTask = Task.Factory.StartNew(Listener, 
TaskCreationOptions.LongRunning);
    }

任務(wù)函數(shù) Listener 創(chuàng)建TcpListener實(shí)例。 AcceptSocketAsync方法等待客戶端連接。一旦客戶端連接,AcceptSocketAsync返回一個(gè)與客戶端關(guān)聯(lián)的套接字。接下來,調(diào)用GetRandomQuoteOfTheDay來使用clientSocket.Send將返回的隨機(jī)引用發(fā)送到客戶端:

photoshop培訓(xùn),電腦培訓(xùn),電腦維修培訓(xùn),移動(dòng)軟件開發(fā)培訓(xùn),網(wǎng)站設(shè)計(jì)培訓(xùn),網(wǎng)站建設(shè)培訓(xùn)

    protected async Task ListenerAsync()
    {      try
      {
        IPAddress ipAddress = IPAddress.Any;
        _listener = new TcpListener(ipAddress, port);
        _listener.Start();        while (true)
        {          using (Socket clientSocket = await _listener.AcceptSocketAsync())
          {            string message = GetRandomQuoteOfTheDay();            var encoder = new UnicodeEncoding();            byte[] buffer = encoder.GetBytes(message);
            clientSocket.Send(buffer, buffer.Length, 0);
          }
        }
      }      catch (SocketException ex)
      {
        Trace.TraceError($"QuoteServer {ex.Message}");        throw new QuoteException("socket error", ex);
      }
    }

photoshop培訓(xùn),電腦培訓(xùn),電腦維修培訓(xùn),移動(dòng)軟件開發(fā)培訓(xùn),網(wǎng)站設(shè)計(jì)培訓(xùn),網(wǎng)站建設(shè)培訓(xùn)

除了Start方法,還需要以下方法,Stop,Suspend和Resume來控制服務(wù):

public void Stop()=> _listener.Stop();public void Suspend()=> _listener.Stop();public void Resume()=> Start();

另一種可以公開獲取的方法是RefreshQuotes。如果包含引用的文件被修改了,則使用此方法重新讀取文件:

  public void RefreshQuotes()=> ReadQuotes();
  }
}

在圍繞服務(wù)器構(gòu)建服務(wù)之前,創(chuàng)建一個(gè)只有QuoteServer實(shí)例并調(diào)用Start的測試程序是非常有用的。這樣可以測試功能又無需處理服務(wù)特定的問題。但必須手動(dòng)啟動(dòng)此測試服務(wù)器,可以使用調(diào)試器輕松遍歷代碼。

測試程序是一個(gè)C??刂婆_(tái)應(yīng)用程序TestQuoteServer。需要引用QuoteServer類的程序集。創(chuàng)建QuoteServer的實(shí)例后,調(diào)用用QuoteServer實(shí)例的Start方法。Start 方法在創(chuàng)建線程后立即返回,因此控制臺(tái)應(yīng)用程序保持運(yùn)行,直到按下Return(代碼文件TestQuoteServer/Program.cs):

photoshop培訓(xùn),電腦培訓(xùn),電腦維修培訓(xùn),移動(dòng)軟件開發(fā)培訓(xùn),網(wǎng)站設(shè)計(jì)培訓(xùn),網(wǎng)站建設(shè)培訓(xùn)

    static void Main()
    {      var qs = new QuoteServer("quotes.txt", 4567);
      qs.Start();
      WriteLine("Hit return to exit");
      ReadLine();
      qs.Stop();
    }

photoshop培訓(xùn),電腦培訓(xùn),電腦維修培訓(xùn),移動(dòng)軟件開發(fā)培訓(xùn),網(wǎng)站設(shè)計(jì)培訓(xùn),網(wǎng)站建設(shè)培訓(xùn)

請注意QuoteServer將在本機(jī)端口4567上運(yùn)行此程序,但以后在客戶端中必須使用配置。

QuoteClient示例

客戶端是一個(gè)簡單的WPF Windows應(yīng)用程序,可以在其中請求來自服務(wù)器的引用。此應(yīng)用程序使用TcpClient類連接到正在運(yùn)行的服務(wù)器并接收返回的消息,顯示在文本框中。用戶界面包含兩個(gè)控件:一個(gè)Button和一個(gè)TextBlock。單擊按鈕從服務(wù)器請求引用,并顯示引用。

使用Button控件,Click事件分配方法OnGetQuote,該方法從服務(wù)器請求引用,并且IsEnabled屬性綁定到EnableRequest方法以在請求處于活動(dòng)狀態(tài)時(shí)禁用該按鈕。使用TextBlock控件,Text屬性綁定到Quote屬性以顯示設(shè)置的引用(代碼文件QuoteClientWPF/MainWindow.xaml):

<Button Margin="3" VerticalAlignment="Stretch" Grid.Row="0"   IsEnabled="{Binding EnableRequest, Mode=OneWay}" Click="OnGetQuote">   Get Quote</Button> <TextBlock Margin="6" Grid.Row="1" TextWrapping="Wrap"   Text="{Binding Quote, Mode=OneWay}" />

類QuoteInformation定義屬性EnableRequest和Quote。這些屬性與數(shù)據(jù)綁定一起使用,以在用戶界面中顯示這些屬性的值。這個(gè)類實(shí)現(xiàn)接口 InotifyPropertyChanged 以使WPF能夠接收屬性值的更改(代碼文件QuoteClientWPF/QuoteInformation.cs):

photoshop培訓(xùn),電腦培訓(xùn),電腦維修培訓(xùn),移動(dòng)軟件開發(fā)培訓(xùn),網(wǎng)站設(shè)計(jì)培訓(xùn),網(wǎng)站建設(shè)培訓(xùn)

using System.Collections.Generic;using System.ComponentModel;using System.Runtime.CompilerServices;namespace Wrox.ProCSharp.WinServices
{  public class QuoteInformation: INotifyPropertyChanged
  {    public QuoteInformation()
    {
      EnableRequest = true;
    }    private string _quote;    public string Quote
    {      get { return _quote; }      internal set { SetProperty(ref _quote, value); }
    }    private bool _enableRequest;    public bool EnableRequest
    {      get { return _enableRequest; }      internal set { SetProperty(ref _enableRequest, value); }
    }    private void SetProperty<T>(ref T field, T value,
                                [CallerMemberName] string propertyName =  null)
    {      if (!EqualityComparer<T>.Default.Equals(field, value))
      {
        field = value;
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
      }
    }    public event PropertyChangedEventHandler PropertyChanged;
  }
}

photoshop培訓(xùn),電腦培訓(xùn),電腦維修培訓(xùn),移動(dòng)軟件開發(fā)培訓(xùn),網(wǎng)站設(shè)計(jì)培訓(xùn),網(wǎng)站建設(shè)培訓(xùn)

注意 接口 INotifyPropertyChanged 的實(shí)現(xiàn)使用屬性CallerMemberNameAttribute。此屬性在第14章“錯(cuò)誤和異?!敝羞M(jìn)行了說明。


作者:沐汐 Vicky

出處:http://www.cnblogs.com/EasyInvoice

歡迎轉(zhuǎn)載,但未經(jīng)作者同意必須保留此段聲明,否則保留追究法律責(zé)任的權(quán)利.

------------------------------------------------------

http://www.cnblogs.com/EasyInvoice/p/6382914.html