2017年12月13日 星期三

委派

透過這篇文章,讓我對委派,終於有所理解
特別摘錄關鍵片段,以利以後複習


感謝原作者的說明,原文 Link 如下

Ref  1:
http://www.huanlintalk.com/2009/01/delegate-revisited-csharp-1-to-2-to-3.html

Ref  2:
http://slashlook.com/articles_20140514.html




===================================================

 C# 委派(delegate)的基本觀念,同時也示範從 C# 1.0、2.0、到 3.0 的委派寫法。 我們會看到更直覺的建立委派物件的語法、匿名方法、以及 Lambda 表示式。

2015-01-15 更新:本文已收錄至電子書《C# 本事》

為什麼要用委派?

從類別設計者的角度來看:在設計類別時,可能會碰到某個方法在執行時需要額外的處理,但你不想/無法將這部份的處理寫死在類別裡(因為變化太多或無法預先得知其處理規則),此時就得將這個部分「外包」給呼叫端。也就是說,呼叫端必須事先提供(註冊)一個函式,等到你的方法在執行時,就會回頭去呼叫(callback)那個事先指定的外包函式。

好,正式一點,我們將外包函式稱為「委派方法」。對類別設計者來說,這種設計方式可將那些變化不定的繁瑣細節從類別中移出去,使類別保持乾淨、穩定。

從呼叫端的角度來看:當你在使用某個類別時,該類別已經設計好一種模式,在你呼叫某個方法之前,它會要求你先提供一個符合特定簽名(signature;即參數與傳回值)的方法,才能達成你想要執行的工作。因此,即使你不是類別設計者,也要了解委派的用法。

傳統的委派寫法

這裡的傳統寫法指的是從 C# 1.0 就提供的委派寫法,這不是說到了 C# 3.0 就全變了樣--基本的程式撰寫模型還是一樣,只是寫法稍有變化。在撰寫委派機制時,基本上都離不開四個步驟:
  1. 宣告委派型別。你需要使用關鍵字 delegate 來定義委派型別的名稱,以及傳入參數和傳回值。
  2. 定義一個符合委派型別的 signature 的方法(可為 instance method 或 static method),這裡簡稱為委派方法。
  3. 建立委派物件,並指定委派方法。
  4. 透過委派物件執行委派方法。
舉例來說,假設我們要設計一個字串串列的類別:StringList(只是為了示範,想必你知道有現成的類別可用了)。我們希望 StringList 提供一個 Find 方法,可以尋找某個符合特定條件的字串,例如:字串中包含特定字元、以特定字元開頭、以特定字元結尾.....等等,可是由於比對條件太多了,如果要寫在類別裡,勢必得提供好幾個方法,例如:FindContains、FindStartsWith、FindEndsWith....等等,而且每碰到一種需求就得再寫一個 Find 版本。比較好的作法,是讓呼叫端來提供字串比對的動作,如此一來,StringList 類別就只需提供一個 Find 方法,這也意味著它提供了一種支援未來(未知)需求的方法。

StringList 類別大概會長這樣:

   1:  public delegate bool Predicate(string s);  // 步驟 1: 定義委派型別.
   2:  
   3:  public class StringList
   4:  {
   5:      // 我知道用 ArrayList 看起來有點笨,但我想還是先不要把泛型扯進來。
   6:      private ArrayList strings;
   7:  
   8:      public StringList()
   9:      {
  10:          // 在建構元裡面就填好字串內容...只是為了示範,實際上通常不會這樣寫.
  11:          strings = new ArrayList();
  12:          strings.Add("Banana");
  13:          strings.Add("Apple");
  14:          strings.Add("Mango");
  15:      }
  16:  
  17:      public string Find(Predicate p)
  18:      {
  19:          for (int i = 0; i < strings.Count; i++)
  20:          {
  21:              string s = (string) strings[i];
  22:              bool isMatch = p(s);  // 步驟 4: 執行委派任務. 等同於 p.Invoke(s)
  23:              if (isMatch)    // 目前的字串符合呼叫端的比對條件?
  24:              {
  25:                  return s;
  26:              }
  27:          }
  28:          return "";  // 找不到,傳回空字串
  29:      }
  30:  }

注意第 1 行的宣告,這一行就是前面說的步驟 1:宣告委派型別。這行程式碼的意思是:定義一個名為 Predicate 的委派型別,而這個委派型別所要「包裝」的函式必須傳入一個字串,並傳回一個布林值,代表該字串是否符合比對條件。 注意我說「委派型別」,是的,雖然只有一行,但這寫法確實是在定義一個類別--編譯器會將它編譯成一個繼承自 System.MulticastDelegate 的類別,而從這個父類別 MulticastDelegate 的名稱便可約略看出,這個委派型別的 instance(以下皆以「委派物件」稱之)可以一次引動(invoke)多個委派方法。這點稍後會再說明。

Find 方法需要傳入一個 Predicate 委派物件,它會用一個 for 迴圈逐一走訪串列中的每個字串,並透過該委派物件得知目前處理的字串是否符合呼叫端的比對條件。這也就是前面說的,StringList 的 Find 方法把字串比對的工作外包給呼叫端了,因為只有呼叫端才知道它想要找甚麼樣的字串。

StringList 類別設計好之後,接著來看用戶端會怎麼使用這個類別。我們會看到前面所說的四個步驟中的後面三個步驟。

由於我們的 StringList 類別已經預先內建了三個字串:"Apple"、"Mango"、"Banana",所以我們可以直接示範尋找以 "go" 結尾的字串。 範例程式碼如下:

   1:  /// <summary>
   2:  /// 示範 C# 1.0 的委派寫法.
   3:  /// </summary>
   4:  public class DelegateDemoVer1
   5:  {
   6:      public void Run()
   7:      {
   8:          StringList fruits = new StringList();
   9:  
  10:          Predicate p = new Predicate(FindMango); // 步驟 3: 建立委派物件
  11:  
  12:          string s = fruits.Find(p);
  13:  
  14:          Console.WriteLine(s);
  15:      }
  16:  
  17:      // 步驟 2: 撰寫符合委派型別所宣告的委派方法。
  18:      bool FindMango(string s)
  19:      {
  20:          return s.EndsWith("go");
  21:      }
  22:  }

注意第 10 行,也就是建立委派物件的程式碼,這行可以這樣理解:建立一個委派物件,這個委派物件會記住(保存)你提供的函式(此處即為 FindMango 方法),以便將來需要時可以呼叫它。但是,前面提過,委派型別是繼承自 System.MulticastDelegate 類別,這隱約透露著委派物件不只能記住一個函式。事實上,委派物件內部有一個串列,所以它能夠存放多個函式參考。如果你還是覺得不太明白,不妨把委派物件想像成一個代理人,這個代理人手上有一份工作清單,而你可以任意加入多項工作到這份清單裡,到時候只要呼叫這個代理人的 Invoke 方法,它就會逐一執行工作清單中的每一項任務。

以剛才的第 10 行程式碼來說,其作用就只是交代一項工作(指定一個函式參考)而已。如果要加入多項工作,就必須使用另一個運算子:+=。例如:

   Predicate p = new Predicate(FindMango);
   p += new Predicate(FindApple);
   p += new Predicate(FindMango);    

其記憶體布局如下圖所示:



我刻意重複加入了 FindMango,是為了強調:委派物件的呼叫清單不會濾掉重複的函式參考,亦即同一個函式可以重複加入多次。當你呼叫委派物件的 Invoke 方法,它就會逐一呼叫清單中的每一個函式。另外要牢記的是:在撰寫程式時,程式的執行結果絕對不可依賴這些委派函式的執行順序;它們的執行順序不見得是你認為的那樣。喔對了,既然有 +=,當然也有 -=;二者寫法相同,只是前者會將函式參考加入呼叫清單,後者則是從清單中移除函式參考。

以上就是 .NET 委派程式設計的基本觀念,也是 .NET 事件訂閱/發行的程式設計模型的基礎。我想談到這裡應該差不多了,接著來看 C# 2.0 和 3.0 的寫法。

C# 2.0 的寫法

C# 2.0 在建立委派物件的語法可以更簡潔、也更直覺。例如前面範例的第 10 行可以改成這樣:

  10:  Predicate p = FindMango; // 步驟 3: 建立委派物件



















2017年12月6日 星期三

資料繫結(DataBinding) - 用List 與 ObservableCollection 作為繫結的資料來源的差異

Ref:
https://dotblogs.com.tw/v6610688/2014/04/07/xaml_csharp_listbox_databinding_by_observablecollection



此篇介紹如何繫結到資料集合的元件,如ListBox、ListView等等的元件。
這些元件都是集合,皆有繼承ItemsControl,所以在繫結實的方式會比較不一樣。

在這邊以ListBox元件作為範例。
並且分別介紹與用List 與 ObservableCollection 作為繫結的資料來源的差異

介紹




在此篇介紹如何繫結到資料集合的元件,如ListBox、ListView等等的元件。
這些元件都是集合,皆有繼承ItemsControl,所以在繫結實的方式會比較不一樣。

在這邊以ListBox元件作為範例。
並且分別介紹與用List<T> 與 ObservableCollection<T> 作為繫結的資料來源的差異

範例介紹:




本範例如用Windows Store App
為了要繫結至ListBox顯示資料的元件,在邏輯端通常會與用List<T>作為保存資料的結構方式,這邊以WantedData類別為例:
用來記錄想要的東西與價錢
public class WantedData {
        public string Content{ get; set; }
        public int Price { get; set; }
    }
首先是XAML中的程式碼,在這邊用了一個ListBox顯示列出來的願望清單
並另外給予新增的部分,分別可以輸入想要的物品名稱與價錢,如下
<ListBox Name="wantedListBox1" FontSize="32" HorizontalAlignment="Left" Height="599" Margin="30,144,0,0" VerticalAlignment="Top" Width="382">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding Content}" FontSize="24"/>
                        <TextBlock Text="{Binding Price}" FontSize="24"/>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>

        <ListBox x:Name="wantedListBox2"  FontSize="32" HorizontalAlignment="Left" Height="599" Margin="481,144,0,0" VerticalAlignment="Top" Width="388">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding Content}" FontSize="24"/>
                        <TextBlock Text="{Binding Price}" FontSize="24"/>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>

        <Button Content="加入清單" FontSize="32" HorizontalAlignment="Left" Margin="921,422,0,0" VerticalAlignment="Top" Height="66" Width="154" Click="Button_Click"/>
        <TextBox Name="wantedText" HorizontalAlignment="Left" Margin="921,211,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Height="48" Width="154"/>
        <TextBlock  HorizontalAlignment="Left" Margin="921,144,0,0" TextWrapping="Wrap" Text="想要的東西:" FontSize="40" VerticalAlignment="Top"/>
        <TextBox x:Name="wantedPriceText" HorizontalAlignment="Left" Margin="921,353,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Height="48" Width="154"/>
        <TextBlock  HorizontalAlignment="Left" Margin="921,281,0,0" TextWrapping="Wrap" Text="價錢:" FontSize="40" VerticalAlignment="Top"/>

重新定義了ListBox的資料樣板DataTemplate,並繫結到WantedData 的資料
介面如下圖:
4-7-listbox-databinding

以下便來介紹如何實作。

透過ObservableCollection<T> 作為繫結的資料來源




一般而言會建議使用ObservableCollection<T> 作為繫結的資料來源,因為ObservableCollection<T>實作了INotifyCollectionChanged介面,相當於前一篇的INotifyPropertyChanged介面,告知UI端繫結的集合資料更改。

1.在MainPage中建立資料

ObservableCollection<WantedData> wantedDataCollection = new ObservableCollection<WantedData>
       {
            new WantedData{Content = "筆記本" , Price = 30},
           new WantedData{Content = "鞋子" , Price = 2000}
       };
這邊是範例資料,如果沒有的話就是new就好。

2.並在建構子設定ItemsSource的繫結來源為wantedDataList

//繫結來源
wantedListBox2.ItemsSource = wantedDataCollection;

3.在按鈕Click時新增資料到wantedDataCollection

//ObservableCollection
wantedDataCollection.Add(new WantedData { Content = wantedText.Text, Price = Convert.ToInt16(wantedPriceText.Text) });

以上完成!因為ObservableCollection幫我們實現了介面,所以我們不用像一般的簡單資料繫結,要去實現介面,只要向上面這樣,三個步驟加上自己要淳放資料的類別建立好即可。

List<T>作為繫結的資料來源




透過List<T>會因為沒有實現INotifyCollectionChanged介面所以要多兩個小步驟

1.初始化

List<WantedData> wantedDataList = new List<WantedData>
       {
           new WantedData{Content = "筆記本" , Price = 30},
           new WantedData{Content = "鞋子" , Price = 2000}
       };

2.建構時繫結來源

//繫結來源
wantedListBox1.ItemsSource = wantedDataList;

3.新增資料時,重新指定ItemsSource來源!

wantedDataList.Add(new WantedData { Content = wantedText.Text ,Price = Convert.ToInt16(wantedPriceText.Text) });

//使用List需要在指定一次
wantedListBox1.ItemsSource = null;
wantedListBox1.ItemsSource = wantedDataList;
因為沒有告知UI更新的關係,所以我們要主動重新在指定一次。

最後的畫面如下;
4-7-listbox-databinding-result

結合更新資料繫結




但是只有這樣,當我們要新增更改資料的話,會無法更改,原因在於,我們的WantedData沒有實作INotifyPropertyChanged的關係,所以當資料被更改時,UI的部分不會被更改
INotifyPropertiesChanged

於是改成如下:
//INotifyPropertyChanged 用來協助更新資料
   public class WantedData : INotifyPropertyChanged
   {
       string content;
       int price;
       public string Content
       {
           get { return content; }
           set
           {
               content = value;
               NotifyPropertyChanged("Content");
           }
       }
       public int Price
       {
           get { return price; }
           set
           {
               price = value;
               NotifyPropertyChanged("Price");
           }
       }

       public event PropertyChangedEventHandler PropertyChanged;
       protected void NotifyPropertyChanged(string propertyName)
       {
           if (PropertyChanged != null)
           { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); }
       }


參考資料:



ObservableCollection

Ref:

https://dotblogs.com.tw/wadehuang36/2010/11/03/observablecollection


System.Collections.ObjectModel.ObservableCollection不是什麼新鮮的東西,早在幾年前,就存在Silverlight與WPF中,它跟一般的Collection最大的不同地方在,它可以被監控,當集合有所變動,會產生事件,可以控制變化,如UI亓件需要監控集合,當變動時UI必需有所調整,或新增資料到集合中,要做一些初始化的動作等等。
NOTE:
在3.0中ObservableCollection建置在WindowsBase.dll中,不過我想沒人有寫個Web AP,為了一個類別而加入WPF的基本組件吧,不過在4.0中ObservableCollection已經被移到System.dll了,不管在什麼類型的專案都可以使用。

 

範例

public interface IModel : INotifyPropertyChanged
{
    string Key { get; set; }
    string Value { get; set; }
}

public class Sample
{
    public Sample()
    {
        ObservableCollection<IModel> collection = new ObservableCollection<IModel>();
        collection.CollectionChanged += OnCollectionChanged;
    }

    private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        //當新增物件到集合時,做一些初始化
        if (e.Action == NotifyCollectionChangedAction.Add)
        {
            foreach (IModel modle in e.NewItems)
            {
                modle.PropertyChanged += (x, y) =>
                {
                    //Do Something
                };
            }
        }
    }
}

NotifyCollectionChangedEventArgs說明

屬性
說明
Action
取得造成事件的動作。
NewItems
取得變更所涉及的新項目清單。
NewStartingIndex
取得發生變更的索引,預設值為-1,如collection.Insert(5,obj),NewStartingIndex值為是5。
OldItems
取得受 Replace、Remove 或 Move 動作影響的項目清單。
OldStartingIndex
取得發生 Move、Remove 或 Replace 動作的索引,預設值為-1,如collection.RemoveAt(5,obj),OldStartingIndex值為是5。

Action種類

成員名稱
說明
Add
集合中已加入一個或多個項目。
Remove
集合中已移除一個或多個項目。
Replace
集合中已取代一個或多個項目。
Move
集合中已移動一個或多個項目。
Reset
集合的內容已大幅變更。

參考資料

base demo

Ref:
https://docs.microsoft.com/zh-tw/dotnet/csharp/language-reference/keywords/base


base (C# 參考)

base 關鍵字是用來存取衍生類別中基底類別的成員︰
  • 對已由另一個方法覆寫的基底類別來呼叫方法。
  • 指定應該在建立衍生類別的執行個體時呼叫的基底類別建構函式。
僅允許在建構函式、執行個體方法或執行個體屬性存取子中進行基底類別存取。
從靜態方法使用 base 關鍵字是錯誤的。
所存取的基底類別是類別宣告中所指定的基底類別。 例如,如果您指定 class ClassB : ClassA,則不論 ClassA 的基底類別為何,都會從 ClassB 存取 ClassA 成員。

範例

在此範例中,基底類別 Person 和衍生類別 Employee 都會有名為 Getinfo 的方法。 使用 base 關鍵字,即可從衍生類別對基底類別呼叫 Getinfo 方法。
C#
public class Person
{
    protected string ssn = "444-55-6666";
    protected string name = "John L. Malgraine";

    public virtual void GetInfo()
    {
        Console.WriteLine("Name: {0}", name);
        Console.WriteLine("SSN: {0}", ssn);
    }
}
class Employee : Person
{
    public string id = "ABC567EFG";
    public override void GetInfo()
    {
        // Calling the base class GetInfo method:
        base.GetInfo();
        Console.WriteLine("Employee ID: {0}", id);
    }
}

class TestClass
{
    static void Main()
    {
        Employee E = new Employee();
        E.GetInfo();
    }
}
/*
Output
Name: John L. Malgraine
SSN: 444-55-6666
Employee ID: ABC567EFG
*/
如需其他範例,請參閱 newvirtual 和 override

範例

這個範例示範如何指定在建立衍生類別的執行個體時呼叫的基底類別建構函式。
C#
public class BaseClass
{
    int num;

    public BaseClass()
    {
        Console.WriteLine("in BaseClass()");
    }

    public BaseClass(int i)
    {
        num = i;
        Console.WriteLine("in BaseClass(int i)");
    }

    public int GetNum()
    {
        return num;
    }
}

public class DerivedClass : BaseClass
{
    // This constructor will call BaseClass.BaseClass()
    public DerivedClass() : base()
    {
    }

    // This constructor will call BaseClass.BaseClass(int i)
    public DerivedClass(int i) : base(i)
    {
    }

    static void Main()
    {
        DerivedClass md = new DerivedClass();
        DerivedClass md1 = new DerivedClass(1);
    }
}
/*
Output:
in BaseClass()
in BaseClass(int i)
*/