顯示具有 WPF 標籤的文章。 顯示所有文章
顯示具有 WPF 標籤的文章。 顯示所有文章

2011年10月14日 星期五

WPF 中如何 Binding 到 UserControl

WPF 中,最好用的莫過於強大的繫結(Binding)能力了。也因此,以往常用的 WinForm 寫作方式也必須作個改變。這裡以 UserControl 的 Binding 為例。

Windows Form 中 UserControl

第一個例子是舊習慣的 UserControl 寫作方式。

首先有個 MyUserControl,功能為:使用 Count propery,顯示出多個人名的下拉選單,讓使用者選擇。並以 SelectedName 作為選擇項目的輸出。

public partial class MyUserControl : UserControl
  {
    public MyUserControl() { InitializeComponent(); }

    public int Count { get; set; }

    public string SelectedName
    {
      get { return cbNames.SelectedValue as string; }
    }

    private void UserControl_Loaded(object sender, RoutedEventArgs e)
    {
      var q = from i in Enumerable.Range(1, Count)
              select string.Format("Name{0}", i);
      cbNames.ItemsSource = q;
    }
  }

使用此 UserControl 時,只需要在 xaml 中加上 Count 的屬性,就可以顯示多個人名。

MainWindow.xaml

<Window x:Class="NormalUserControl.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525" xmlns:my="clr-namespace:NormalUserControl">
  <Grid>
    <my:MyUserControl HorizontalAlignment="Left" Margin="13,11,0,0" x:Name="myUserControl1" VerticalAlignment="Top" Count="3" />
    <Button Content="Ok" Height="23" HorizontalAlignment="Left" Margin="25,87,0,0" Name="button1" VerticalAlignment="Top" Width="75" Click="button1_Click" />
  </Grid>
</Window>

最後,在 Button Click event handler 中,如下的使用方式,可以知道使用者選擇了何人。
private void button1_Click(object sender, RoutedEventArgs e)
{
  MessageBox.Show(myUserControl1.SelectedName);
}

一切似乎運作的很好。這也是 Windows Form 的寫作方式。

有個問題來了,如果這個 UserControl 的 Count 值,是繫結自另一個控制項呢?

如下:

<Window x:Class="NormalUserControl.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525" xmlns:my="clr-namespace:NormalUserControl">
  <Grid>
    <my:MyUserControl HorizontalAlignment="Left" Margin="13,11,0,0" x:Name="myUserControl1" VerticalAlignment="Top" Count="{Binding}" />
    <Button Content="Ok" Height="23" HorizontalAlignment="Left" Margin="23,108,0,0" Name="button1" VerticalAlignment="Top" Width="75" Click="button1_Click" />
    <TextBox x:Name="txtCount" Margin="160,12,247,265" Text="3" />
  </Grid>
</Window>
我們想到將 Count 的值繫結自一個 txtCount 的 TextBox,然而 Visual Studio IDE 卻告訴我,A 'Binding' cannot be set on the 'Count' property of type 'MyUserControl'. A 'Binding' can only be set on a DependencyProperty of a DependencyObject. image

WPF : Binding 到 Control

在 WPF 中,DependencyObject 的屬性如果要被繫結的話,該屬性必須是 DependencyProperty.

修改的方式也不難。首先在MyUserControl.xaml.cs 中使用 dependencyProperty 的 snippet,作一個 Count 的 dependencyProperty, 取代原先的 Count property

image

public static readonly DependencyProperty CountProperty =
  DependencyProperty.Register("Count", typeof(int), typeof(MyUserControl), new PropertyMetadata(default(int))
  );

public int Count
{
  get { return (int)GetValue(CountProperty); }
  set { SetValue(CountProperty, value); }
}

再執行一次,程式可以正常編譯了。

PropertyChangedCallback

執行程式,修改 TextBox為另一個數字後發現,Binding 只有第一次執行時才會繫結,其他時間都不會。為什麼?

原因是目前的實作,並沒有告訴它「當資料變化時該怎麼執行」。

以下是修改後的結果。

public static readonly DependencyProperty CountProperty =
  DependencyProperty.Register("Count", typeof(int), typeof(MyUserControl), 
  new UIPropertyMetadata(default(int), OnCountChanged));

private static void OnCountChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
  var count = (int)e.NewValue;
  var q = from i in Enumerable.Range(1, count)
          select string.Format("Name{0}", i);
  var userControl = o as MyUserControl;
  var comboBox = userControl.FindName("cbNames") as ComboBox;
  comboBox.ItemsSource = q;
}

以上的重點在於,註冊 DependencyProperty 時,告訴 WPF,當 Count 的值有變化時,請執行 OnCountChanged.

UserControl 輸出的 Binding

上面談完了將值 Binding 到 UserControl,UserControl 可以讀值,並顯示 UI。那輸出的值是否也可以 Binding 呢?如下面的程式,我們發現 UserControl 的SelectedName 雖然可以讀出,但也不能做 DataBinding。

<Window x:Class="NormalUserControl.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525" xmlns:my="clr-namespace:NormalUserControl">
  <Grid>
    <my:MyUserControl HorizontalAlignment="Left" Margin="13,11,0,0" x:Name="myUserControl1" VerticalAlignment="Top" Count="{Binding ElementName=txtCount, Path=Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
    <Button Content="Ok" Height="23" HorizontalAlignment="Left" Margin="23,108,0,0" Name="button1" VerticalAlignment="Top" Width="75" Click="button1_Click" />
    <TextBox x:Name="txtCount" Margin="160,12,247,265" Text="4" />
    <Label Content="{Binding SelectedName, ElementName=myUserControl1}" Height="28" HorizontalAlignment="Right" Margin="0,101,247,0" Name="label1" VerticalAlignment="Top" />
    <Label Content="Your Select" Height="28" HorizontalAlignment="Left" Margin="150,101,0,0" Name="label2" VerticalAlignment="Top" />
  </Grid>
</Window>

此時,要解這個問題,SelectedName仍然需要 dependencyproperty。

public string SelectedName
{
  get { return (string)GetValue(SelectedNameProperty); }
  set { SetValue(SelectedNameProperty, value); }
}

public static readonly DependencyProperty SelectedNameProperty =
    DependencyProperty.Register("SelectedName", typeof(string), typeof(MyUserControl),
    new UIPropertyMetadata(null));

 

光這樣是不夠的。ComboBox 的 SelectedItem 並不會自動更新這我們剛剛加上的 SelectedName。

此時在建構子上加如下的繫結(只不過是用程式加的)

var binding = new Binding("SelectedName") { Source = this };
  cbNames.SetBinding(ComboBox.SelectedItemProperty, binding);

 

結論

當 UserControl 遇到 Binding 時,會發生許多原先在 Windows Form 上遇不到的事。正因為 WPF 的繫結非常強大,要駕馭它也要相當的功夫。

2011年7月7日 星期四

ASP.NET WebForm 與 WPF 在取得使用者輸入資料時的差異

緣由

這個標題下的有些古怪。

我的專案,原本都以 ASP.NET為主。排程的程式大都是 Console 的程式。然而最近接了一個 Client Application 的專案。

經過比較後,我選擇了較新的 WPF,而放棄了 WinForm。問題來了,專案中的成員會問到WPF中如何在DataGrid 取得使用者所選擇ComboBox 的SelectedIndex。

為了演示 WebForm 與 WPF 兩者的差異,我寫了這個 demo.

這個 Demo其實很簡單:設定10個 Emp 的性別。然而分別以 WebForm 與 WPF 做一次,再討論兩者的設計方式。

資料來源

雖然是不同的應用程式,但有相同的資料,故,將兩者相同的程式放在 CommonLib 中.

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

namespace CommonLib
{
  public class Emp
  {
    public int Id { get; set; }
    public string Name { get; set; }
    public int Gender { get; set; }
  }
  public class GenderStruct
  {
    public int Id { get; set; }
    public string Name { get; set; }
  }
  public class MyDataSource
  {
    public List<Emp> GetAllEmps()
    {
      var q = from i in Enumerable.Range(1, 10)
              select new Emp()
              {
                Id = i,
                Name = string.Format("N{0}", i),
                Gender = 1
              };
      return q.ToList();
    }

    public List<GenderStruct> GetAllGenders()
    {
      return new List<GenderStruct>
                      {
                        new GenderStruct { Id = 1, Name = "Boy"},
                        new GenderStruct { Id = 2, Name = "Girl"},
                        new GenderStruct { Id = 3, Name = "Gay"},
                      };
    }
  }
 
}

WebForm

ASP.NET WebForm的設計其實真的很麻煩,但用習慣了,也就習以為常。下是在 aspx 中放入一個 GridView 以顯示員工的資料。使用者輸入性別再按下 button,網頁顯示所有員工的最後值。

Default.aspx

<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" 
  DataKeyNames="Id" onrowdatabound="GridView1_RowDataBound">
  <Columns>
    <asp:BoundField DataField="Id" HeaderText="Id" />
    <asp:BoundField DataField="Name" HeaderText="Name" />
    <asp:TemplateField HeaderText="Gender">
      <ItemTemplate>
        <asp:DropDownList runat="server" ID="ddl">
        </asp:DropDownList>
      </ItemTemplate>
    </asp:TemplateField>
  </Columns>
</asp:GridView>
<asp:Button ID="Button1" runat="server" onclick="Button1_Click" Text="Button" />

Default.aspx.cs

using System;
using System.Web.UI.WebControls;
using CommonLib;

namespace WebApplication1
{
  public partial class _Default : System.Web.UI.Page
  {
    protected void Page_Load(object sender, EventArgs e)
    {
      if (!Page.IsPostBack)
      {
        var dataSource = new MyDataSource();
        var emps = dataSource.GetAllEmps();
        GridView1.DataSource = emps;
        GridView1.DataBind();
      }
    }

    protected void GridView1_RowDataBound(object sender, GridViewRowEventArgs e)
    {
      var dataSource = new MyDataSource();
      var allGender = dataSource.GetAllGenders();
      if (e.Row.RowType == DataControlRowType.DataRow)
      {
        var ddl = e.Row.FindControl("ddl") as DropDownList;
        ddl.DataSource = allGender;
        ddl.DataValueField = "Id";
        ddl.DataTextField = "Name";
        ddl.DataBind();
      }
    }

    protected void Button1_Click(object sender, EventArgs e)
    {
      //由 GridView1 讀取資料
      foreach (GridViewRow row in GridView1.Rows)
      {
        var ddl = row.FindControl("ddl") as DropDownList;
        var gender = ddl.SelectedIndex;
        var id = int.Parse(row.Cells[0].Text);
        string message = string.Format("Id:{0}, Gender:{1}", id, gender);
        Response.Write(message + "<br />");
      }
    }
  }
}

這一段程式寫的還真不怎麼樣,竟然還用 Response.Write 這種古老 ASP 的寫作方式?重點不是在演示如何取得資料,而是 ASP.NET 先天上的限制: Stateless

由於 Web 的 Stateless 的特性,使得Server 端不會記得 Client 曾經輸入或給過的值。因此,當網頁第一次進入時取得所有 Emp 的資料,並在 GridView 的 RowDataBound 事件發生時,為每個性別欄位設定 DropDownList。

之後,使用者輸入完畢後,Submit 回到 Server後,emp 及 gender 的資料都已經不見了,只剩下 GridView 這個 Control 中的值能幫我們留下使用者最終的選擇。

在不得已的情況下,我們必須研究 GridView 的組成,如 GridView 的 Row,Row 下有 Cells,再使用 FindControl 取得 DropDownList…等等。image

WPF

在 WPF 中,由於是 Stateful,相同的功能,寫作方式相對 ASP.NET 大為進步。

MainForm.xaml

<DataGrid AutoGenerateColumns="False" Height="206" HorizontalAlignment="Left" Margin="12,12,0,0" Name="dataGrid1" VerticalAlignment="Top" Width="288" >
  <DataGrid.Columns>
    <DataGridTextColumn Header="Id" Binding="{Binding Path=Id}" />
    <DataGridTextColumn Header="Name" Binding="{Binding Path=Name}" />
    <DataGridTextColumn Header="Gender" Binding="{Binding Path=Gender}" />
    <DataGridComboBoxColumn Header="Gender2" x:Name="cbGender" SelectedValuePath="Id" DisplayMemberPath="Name" SelectedValueBinding="{Binding Gender}"   />
  </DataGrid.Columns>
</DataGrid>
<Button Content="Button" Height="23" HorizontalAlignment="Left" Margin="359,115,0,0" Name="button1" VerticalAlignment="Top" Width="75" Click="button1_Click" />

MainForm.xaml.cs

public partial class MainWindow : Window
{
private List<Emp> _emps;
private List<GenderStruct> _allGenders;
public MainWindow()
{
  InitializeComponent();
}

private void Window_Loaded(object sender, RoutedEventArgs e)
{
  //取得資料並放到 Window 的變數中。因為Stateful,所以可以這樣做
  var dataSource = new MyDataSource();
  _emps = dataSource.GetAllEmps();
  _allGenders = dataSource.GetAllGenders();

  dataGrid1.ItemsSource = _emps;
  cbGender.ItemsSource = _allGenders;
}

private void button1_Click(object sender, RoutedEventArgs e)
{
  var emp = dataGrid1.SelectedItem as Emp;
  var message = string.Format("Name: {0}, Gender: {1}", emp.Name, emp.Gender);
  MessageBox.Show(message);
}
}

值得注意的是,在 Window Loaded 後,取得所有 Emp 及 Gender 資料後,直接放到 Window 的 private member 中,讓後續的資料繫結(databind) 作業直接存取window private member即可。

使用者輸入完畢後,更新後的資料「直接」反應到資料繫結的對象(即 _emps, _allGenders),不必像 WebForm一樣透過 Control 間接取得資料。

image

結論

這個範例中,主要演示了 ASP.NET WebForm 與 WPF 的先天上的差異(Stateless vs Stateful),並且由於 WPF 雙向繫結的功能,使用者的輸入可以直接更新到資料來源,遠較WebForm 來的直覺與方便。

2011年6月22日 星期三

WPF的UI更新方式

緣由

在以往的 VB6,或者是 Windows Form 應用程式中,更新 UI的方式極為簡單,往往只是 Application.DoEvents 就可以更新。Windows Form 中更有 InvokeBeginInvoke 可以彈性地使用。

那在 WPF 中,要如何更新 UI 的內容呢?

範例1:Bad Example

當然,要從一個不正確的範例開始。

Ex1Bad.xaml

<Window x:Class="WpfApplication10.Ex1Bad"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Ex1Bad" Height="300" Width="300">
  <StackPanel>
    <Label Name="lblMsg" Content="Nothing" />
    <Button Content="Start" Name="btnStart" Click="btnStart_Click"/>
  </StackPanel>
</Window>

Ex1Bad.xaml.cs

using System.Threading;
using System.Windows;

namespace WpfApplication10
{
  public partial class Ex1Bad : Window
  {
    public Ex1Bad()
    {
      InitializeComponent();
    }

    private void btnStart_Click(object sender, RoutedEventArgs e)
    {
      lblMsg.Content = "Starting...";
      Thread.Sleep(3000); //執行長時間工作
      lblMsg.Content = "Doing...";
      Thread.Sleep(3000); //執行長時間工作
      lblMsg.Content = "Finished...";
    }
  }
}

這裡以 Thread.Sleep(3000)讓 UI Thread 睡個3秒鐘,來模擬長時間的工作。

這是個常見的程式碼,但卻是沒有用。在 Windows Form 的 API 中有 Application.DoEvents() 可以呼叫。WPF 中沒有類似的嗎?

範例2:使用Windows Form的 DoEvents

原來,WPF 中雖然沒有類似的 api 可以呼叫,但仍可以直接呼叫 Windows Form 的 Application.DoEvents.當然,需要引用 System.Windows.Forms.dll。

Ex2WinformDoEvents.xaml

using System.Threading;
using System.Windows;
using swf = System.Windows.Forms;

namespace WpfApplication10
{
  public partial class Ex2WinformDoEvents : Window
  {
    public Ex2WinformDoEvents()
    {
      InitializeComponent();
    }

    private void btnStart_Click(object sender, RoutedEventArgs e)
    {
      lblMsg.Content = "Starting...";
      swf.Application.DoEvents();
      Thread.Sleep(3000); //執行長時間工作
      lblMsg.Content = "Doing...";
      swf.Application.DoEvents();
      Thread.Sleep(3000); //執行長時間工作
      lblMsg.Content = "Finished...";
    }
  }
}

在更新UI後,呼叫 swf.Application.DoEvents(),就可以更新 UI 了。這樣的方式與之前的 VB6是一模一樣的手法。

範例3:WPF DoEvents

哦?WPF 沒有 DoEvents 可以使用,只能呼叫老前輩Windows Form 的 API 嗎?也不是。在 DispacherFrame 文章中就有sample.

Ex3WPFDoEvents.xaml.cs

using System;
using System.Security.Permissions;
using System.Threading;
using System.Windows;
using System.Windows.Threading;

namespace WpfApplication10
{
  public partial class Ex3WPFDoEvents : Window
  {
    public Ex3WPFDoEvents()
    {
      InitializeComponent();
    }

    private void btnStart_Click(object sender, RoutedEventArgs e)
    {
      lblMsg.Content = "Starting...";
      DoEvents();
      Thread.Sleep(3000); //執行長時間工作
      lblMsg.Content = "Doing...";
      DoEvents();
      Thread.Sleep(3000); //執行長時間工作
      lblMsg.Content = "Finished...";
    }

    [SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)]
    public void DoEvents()
    {
      var frame = new DispatcherFrame();
      Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
          new DispatcherOperationCallback(ExitFrame), frame);
      Dispatcher.PushFrame(frame);
    }

    public object ExitFrame(object f)
    {
      ((DispatcherFrame)f).Continue = false;

      return null;
    }

    public static void DoEvents2()
    {
      Action action = delegate { };
      Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Input, action);
    }
  }
}

DoEvents() 與 DoEvents2() 的效果相同。

DoEvents is Evil

DoEvents 這麼好用,為什麼WPF還要發明 Dispatcher,或 Windows Form 的 BeginInvoke 這些 API  呢?

跑以下的程式碼看看吧。

private void btnEvil_Click(object sender, RoutedEventArgs e)
{
  for (int i = 0; i < 10000000; i++)
  {
    System.Windows.Forms.Application.DoEvents();
  }
  MessageBox.Show("Ok");
}

執行時,記得打開工作管理員看看CPU 的負載,會持續飆高一斷時間。雖然 UI 沒有任何的更新,為何 CPU 會飆高呢?

DoEvent 的原理是execution loop,也就是以迴圈的方式來查詢是否有要更新的訊息(message)。一看到迴圈,各位看倌就知道是怎麼回事了吧。

範例3中的WPF 的 DoEvents 也是相同的道理。

範例4:BackgroundWorker

有沒有較正常的方式來更新 UI 呢?看一下Ex1Bad.xaml.cs的設計方式吧。更新lblMessage後執行一段工作,這基本上是同步的寫作方式。在 UI Thread 上執行工作,本來就會使得 UI 停頓,使用者感到不方變。

正確的方式,是使用BackgroundWorker來執行長時間的工作,並以非同步的方式更新在 UI Tread 上的UI內容。

Ex4BackgroundWorker.xaml.cs

using System;
using System.ComponentModel;
using System.Threading;
using System.Windows;
using System.Windows.Controls;

namespace WpfApplication10
{
  public partial class Ex4BackgroundWorker : Window
  {
    public Ex4BackgroundWorker()
    {
      InitializeComponent();
    }

    private void btnStart_Click(object sender, RoutedEventArgs e)
    {
      ExecuteLongTimeWork(lblMsg, "Starting");
      ExecuteLongTimeWork(lblMsg, "Doing");
      ExecuteLongTimeWork(lblMsg, "Finished");
    }

    private void ExecuteLongTimeWork(Label label, string message)
    {
      var backgroundWorker = new BackgroundWorker();
      backgroundWorker.DoWork += (s, o) => {
        Thread.Sleep(3000); //執行長時間工作
      };

      backgroundWorker.RunWorkerCompleted += (s, args) =>
                                               {
                                                 Dispatcher.BeginInvoke(new Action(() =>
                                                 {
                                                   label.Content = message;
                                                 }));
                                               };
      backgroundWorker.RunWorkerAsync();
    }
  }
}

BackgroundWorker 工作方式,是建立一個新的 Thread 來執行 DoWork 的event handler,執行完畢後,再執行 RunWorkerCompleted 的event handler。因此,我們需要的是在RunWorkerCompleted 的event handler 中更新的 UI。

更新 UI 時,又使用 Dispatcher 來更新 UI。基本上,Dispatcher 是一個 Queue 的概念,凡是使用 Dispatcher 來更新 UI,WPF 會照 DispatcherPriority 的優先順序來更新 UI。

結論

雖然只是小小的UI 更新方式,也是有不少的學問呢!

2011年5月20日 星期五

DataGrid 在 StackPanel 下的效能問題

一個無意間,觸碰到了地雷。而這個地雷,是有原因的。

WPF 的 xaml 中,如果在 StackPanel 中含有 DataGrid,是常見的事。但是,如果 DataGrid bindind 到大量的資料時,就有些問題了。

程式

下面是我的 xaml

xaml

<Window x:Class="LargeDataInDataGrid.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525" Loaded="Window_Loaded">
  <StackPanel>
    <Label Content="資料筆數" />
    <TextBox Name="txtRecNo" TextChanged="txtRecNo_TextChanged" Text="100" />
    <DataGrid Name="dgResult" ItemsSource="{Binding}"/>
  </StackPanel>
</Window>

 

程式運作如下圖

SNAGHTML575c70

現象

當資料只有100筆時,看起來很正常。但看一下工作管理員,天啊,要秏掉記憶體 72 MB。WPF 這麼秏 memory?

image

如果將筆數調整到 1000筆呢?剛改完數字,WPF 程式彷彿當掉一樣。hang 住個幾秒鐘後,看一下記憶體,增加到 187 MB了。

SNAGHTML5b7c97

image

原因

原來, StackPanel 的用途,是儘量地長出 Child control 所要使用的空間 (space)。DataGrid 要長出 100 筆資料的空間,就計算並配置所要的空間及記憶體。所以當 DataGrid 要長出1000筆時,記憶體的使用量就不得了。

但是,我們看不到這麼多筆啊?雖然看不到, StackPanel 仍然堅持配置記憶體呢!

我們將 StackPanel 換成 Grid來試試看。

<Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="30"/>
      <RowDefinition Height="30"/>
      <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <Label Content="資料筆數"  />
    <TextBox Name="txtRecNo" Grid.Row="1" TextChanged="txtRecNo_TextChanged" Text="100" />
    <DataGrid Name="dgResult" ItemsSource="{Binding}" Grid.Row="2"/>
  </Grid>

 

換成 Grid 後,即使是1000筆,記憶體只用了 71 MB,明顯地少掉許多,程式運作時也不會有問題。

image

為什麼 Grid 可以運作正常呢?原來 Grid 運作時是有計算Row 的高度的。DataGrid 在顯示時 Row 高度明顯有限制,因此要求 DataGrid 有高度的限制。DataGrid 的 Height 預設是 Auto,不足時 ScrollBar 就會出現。此時 UI Virtulization 的機制開始運作,只需要顯示看的到的區域即可 ,因此記憶體使用量變少。

如果我還是需要在 StackPanel 使用 DataGrid 呢?只需要設定 DataGrid 的 Height 或 MaxHeight 就可以了。

<StackPanel>
      <Label Content="資料筆數" />
      <TextBox Name="txtRecNo" TextChanged="txtRecNo_TextChanged" Text="100" />
      <DataGrid Name="dgResult" ItemsSource="{Binding}" MaxHeight="500"/>
    </StackPanel>

2011年1月28日 星期五

WPF 學習:自動更新

WPF 的應用程式如何自動更新呢?照目前微軟的建議,會使用ClickOnce 的方式來部署。之後,程式就可以照ClickOnce的方式來更新了。

以下是簡單的範例。

範例

xaml

<Window x:Class="AutoUpdate.MainWindow"
                xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                Title="MainWindow" Height="350" Width="525">
    <Grid>
        <StackPanel Name="panel">
        <TextBlock Text="V1" />
        </StackPanel>
    </Grid>
</Window>

code

using System.Deployment.Application;
using System.Windows;
using System.Windows.Controls;

namespace AutoUpdate
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            AutoUpdateIt();
        }

        private void AutoUpdateIt()
        {
            if (ApplicationDeployment.IsNetworkDeployed)
            {
                var ad = ApplicationDeployment.CurrentDeployment;
                var info = ad.CheckForDetailedUpdate();
                if (info.UpdateAvailable)
                {
                    UpdateApp();
                    DisplayRestartMessage();
                }
            }
        }

        private void UpdateApp()
        {
            ApplicationDeployment ad = ApplicationDeployment.CurrentDeployment;
            ad.Update();
        }

        private void DisplayRestartMessage()
        {
            var newText = new TextBlock { Text = "程式已經更新。請重新啟動程式" };
            panel.Children.Add(newText);

            var button = new Button() { Content = "Restart" };
            button.Click += (sender, e) => Restart(sender, null);
            panel.Children.Add(button);
        }

        private void Restart(object sender, RoutedEventArgs e)
        {
            System.Windows.Forms.Application.Restart();
            Application.Current.Shutdown();
        }
    }
}

程式非常簡單。當程式運作時,發現了有新的版本,就自動下載並更新。更新完後,顯示訊息並讓使用者按「Restart」鍵重新啟動程式。

SNAGHTMLf15e48

範例程式下載

2010年12月9日 星期四

WPF TreeView 的 DataBinding (2)

上一回的 WPF TreeView 的 DataBinding (1),TreeView繫結的是單一型別物件的樹狀結構。這一次要挑戰的是同時繫結到兩種型別。

資料

和上次不同的,這一次目錄 (MyFolder)這個容器(container)中,可同時持有MyFolder 與 MyFile。為此,我們修改程式如下

public static List<MyFolder> GetAllFolder()
{
    var folders = new List<MyFolder>()
                      {
                          new MyFolder("A")
                              {
                                                Files = new List<MyFile>() { new MyFile("d_1")},
                                                SubFolders = new List<MyFolder>(){
                                  new MyFolder("A1")
                                      {
                                                        Files = new List<MyFile>() { new MyFile("d1_1"), new MyFile("d1_2")},
                                                        SubFolders = new List<MyFolder>()
                                                                         {
                                                                             new MyFolder("A11"),
                                                                                        new MyFolder("A12"),
                                                                                        new MyFolder("A13"),
                                                                                        new MyFolder("A14"),
                                                                         }
                                      },
                                                new MyFolder("A2"),
                                                new MyFolder("A3")
                                                }
                              },
                                            new MyFolder("B")
                                                {
                                                    Files = new List<MyFile>() { new MyFile("db_1"), new MyFile("db_2")},
                                                    SubFolders = new List<MyFolder>(){
                                  new MyFolder("B1"),
                                                new MyFolder("B2"),
                                                new MyFolder("B3"),
                                                new MyFolder("B4"),
                                                }
                                            }
                      };
    return folders;
}

繫結

.NET Framework 的元件(control)在進行繫結時,只能繫結到單一物件或同一類別的集合。因此,TreeView 要同時顯示 MyFolder 或 MyFile 時,ItemsSource 給什麼值呢?是 SubFolders 嗎?還是 Files 嗎?都只能顯示一部份。為了讓 ItemsSource 在繫結時可以繫結到所有的children,我們必須製作一個新的屬性 Items,可讀取到所有的children,包含MyFolder 及 MyFiles。故程式碼修改如下

public abstract class MyItem
{
    public string Name { get; set; }
}
public class MyFolder : MyItem
{
    public MyFolder(string name)
    {
        Name = name;
    }

    public List<MyFolder> SubFolders { get; set; }
    public List<MyFile> Files { get; set; }

    public List<MyItem> Items
    {
        get
        {
            var items = new List<MyItem>();
            items.AddRange(SubFolders.AsEnumerable());
            items.AddRange(Files.AsEnumerable());
            return items;
        }
    }
}

xaml 修改如下

<Window x:Class="WpfTreeView1.MainWindow"
                xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:WpfTreeView1="clr-namespace:WpfTreeView1" Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <WpfTreeView1:FolderHelper x:Key="helper" />
    </Window.Resources>
    <Grid>
        <TreeView Name="treeView1" ItemsSource="{Binding Source={StaticResource ResourceKey=helper}, Path=Items}">
            <TreeView.ItemTemplate>
                <HierarchicalDataTemplate ItemsSource="{Binding Items}">
                    <TextBlock TextWrapping="Wrap" Text="{Binding Name}"/>
                </HierarchicalDataTemplate>
            </TreeView.ItemTemplate>
        </TreeView>
    </Grid>
</Window>

注意到我們ItemsSource改繫結到 Items。程式運作結如果附圖image

圖示

上面運作的結果是正確的,但風格還是無法讓使用者接受。原因在於無法分辨何者為目錄,何者為檔案。

但怎麼改呢?HierarchicalDataTemplate 只有一個,如何才能分開顯示目錄及檔案呢?

image

答案是實作一個 DataTemplateSelector。

DataTemplateSelector 的想法是這樣:資料繫結的過程中,當遇到某類型的物件時,套用指定的樣版。而這樣的邏輯是完全客製化的,因此必須自己實作。在 WPF 中,就是繼承DataTemplateSelector, 實作自己的 SelectTemplate。程式如下

using System.Windows;
using System.Windows.Controls;

namespace WpfTreeView1
{
    public class MyDataTemplateSelector : DataTemplateSelector
    {
        public HierarchicalDataTemplate FileTemplate { get; set; }
        public HierarchicalDataTemplate FolderTemplate { get; set; }
        public override System.Windows.DataTemplate SelectTemplate(object item, System.Windows.DependencyObject container)
        {
            if (item is MyFolder)
                return FolderTemplate;
            else
                return FileTemplate;
        }
    }
}

xaml

<Window x:Class="WpfTreeView1.MainWindow"
                xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:WpfTreeView1="clr-namespace:WpfTreeView1" Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <WpfTreeView1:FolderHelper x:Key="helper" />
        <HierarchicalDataTemplate x:Key="folder" ItemsSource="{Binding Items}">
            <StackPanel Orientation="Horizontal">
                <Image Source="/WpfTreeView1;component/Images/folder.png" />
                <TextBlock TextWrapping="Wrap" Text="{Binding Name}"/>
            </StackPanel>
        </HierarchicalDataTemplate>
        <HierarchicalDataTemplate x:Key="file">
            <StackPanel Orientation="Horizontal">
                <Image Source="/WpfTreeView1;component/Images/doc.png" />
                <TextBlock TextWrapping="Wrap" Text="{Binding Name}"/>
            </StackPanel>
        </HierarchicalDataTemplate>
        <WpfTreeView1:MyDataTemplateSelector x:Key="selector" FolderTemplate="{StaticResource ResourceKey=folder}" FileTemplate="{StaticResource ResourceKey=file}" />
    </Window.Resources>
    <Grid>
        <TreeView Name="treeView1" ItemsSource="{Binding Source={StaticResource ResourceKey=helper}, Path=Items}"
                            ItemTemplateSelector="{StaticResource ResourceKey=selector}">
        </TreeView>
    </Grid>
</Window>
程式運行結果如下

image

結論

WPF 已經幫我們想到非常多的問題與解決方式。在解決問題的時候,千萬不要一味的自己發明輪子,最後又難以維護。

程式碼下載

WPF TreeView 的 DataBinding (1)

一樣的問題,在 Silverlight 的版本如何在 WPF 上實作呢?其實作法是一致的。

實作1

xaml

<Window x:Class="WpfTreeView1.MainWindow"
                xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                Title="MainWindow" Height="350" Width="525">
    <Grid>
        <TreeView Name="treeView1">
            <TreeView.ItemTemplate>
                <HierarchicalDataTemplate ItemsSource="{Binding SubFolders}">
                    <TextBlock TextWrapping="Wrap" Text="{Binding Name}"/>
                </HierarchicalDataTemplate>
            </TreeView.ItemTemplate>
        </TreeView>
    </Grid>
</Window>
cs
using System.Windows;

namespace WpfTreeView1
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            treeView1.ItemsSource = FolderHelper.GetAllFolder();
        }
    }
}
執行結果如下圖: image

實作2: 改用 Resource 讀取 Folder

在 cs 中,只有一行的程式 treeView1.ItemsSource = FolderHelper.GetAllFolder(); 實在不美觀。這裡使用在介紹的static binding小技巧,將這一行消除掉。

xaml

<Window x:Class="WpfTreeView1.MainWindow"
                xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:WpfTreeView1="clr-namespace:WpfTreeView1" Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <WpfTreeView1:FolderHelper x:Key="helper" />
    </Window.Resources>
    <Grid>
        <TreeView Name="treeView1" ItemsSource="{Binding Source={StaticResource ResourceKey=helper}, Path=AllFolders}">
            <TreeView.ItemTemplate>
                <HierarchicalDataTemplate ItemsSource="{Binding SubFolders}">
                    <TextBlock TextWrapping="Wrap" Text="{Binding Name}"/>
                </HierarchicalDataTemplate>
            </TreeView.ItemTemplate>
        </TreeView>
    </Grid>
</Window>

cs

using System.Windows;

namespace WpfTreeView1
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }
}

當然,程式運作的結果是一致的。

程式碼下載

2010年9月5日 星期日

WPF 學習:Static Binding

WPF 的強項當然要說到 Biding 了。然而 Data Binding 在 .NET 平台上到處可見。WPF 的 Binding 到底有什麼特別的呢?

下面是一個重構的過程。由最開始由未使用 Binding,一直到使用 Binding 完成後,您可以見到該 Binding 的威力。

需求

身為一個使用者,我希望可以自行挑選表單的背景顏色

第一版:未 Binding

第一個版本是類似 Windows Form 的實作方式。首先打開 Visual Studio 2010, 建立一個 WPF 的應用程式。建立專案後,開啟 MainWindows.xaml,拖進一個 ListBox,並命名為 ColorListBox。如下圖

image

在 Code Behind 的 MainWindows.xaml.cs 上,將程式MainWindows 的建構子修改如下

public MainWindow()
{
    InitializeComponent();

    var props = typeof(Brushes).GetProperties();
    var q = from p in props
            select new
            {
                Name = p.Name,
                Brush = (Brush)p.GetValue(null, null)
            };
    ColorListBox.ItemsSource = q;
    ColorListBox.SelectedValuePath = "Brush";
    ColorListBox.DisplayMemberPath = "Name";
}

在 MainWindow 的建構子中,已經使用了 DataBinding 技術,如同 Windows Form 相同。接下來在 ColorListBox 上雙擊左鍵滑鼠兩下。並修改 EventHandler 如下

private void ColorListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    this.Background = ColorListBox.SelectedValue as Brush;
}

到這裡,程式已經可以運作,而且程式碼相當乾淨。 Code Review 時,見到這樣的程式已經要感謝上天了。

程式運作起來,如下圖.

image

第二版:重構,建立一個 NamedBrush

第一版的程式可個缺點:建構子太長,而且內含非 UI 的邏輯,即使用 Reflection 讀取所有 Brushes 的 public property。
這一個重構的目的,就是將這一段程式拆出來。改放到一個 NamedBrush 類別。

using System.Linq;
using System.Windows.Media;

namespace WpfApplication2
{
    public class NamedBrush
    {
        public Brush Brush { get; set; }
        public string Name { get; set; }
        public static NamedBrush[] All { get; set; }

        static NamedBrush()
        {
            var props = typeof(Brushes).GetProperties();
            var q = from p in props
                    select new NamedBrush
                    {
                        Name = p.Name,
                        Brush = (Brush)p.GetValue(null, null)
                    };
            All = q.ToArray();
        }
    }
}

而 MainWindow.xaml.cs 就可以簡化如下

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        ColorListBox.ItemsSource = NamedBrush.All;
        ColorListBox.SelectedValuePath = "Brush";
        ColorListBox.DisplayMemberPath = "Name";
    }

    private void ColorListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        this.Background = ColorListBox.SelectedValue as Brush;
    }
}

第三版:在xaml中設定屬性

為了簡少Code behind 的程式,我們將建構子中的 ColorListBox 的屬性值設定陳述句移到 xaml 中。 也就是將下面這兩行

ColorListBox.SelectedValuePath = "Brush";
ColorListBox.DisplayMemberPath = "Name";

移到 xaml 中,如下

<ListBox Height="213" HorizontalAlignment="Left" Margin="76,12,0,0" Name="ColorListBox" VerticalAlignment="Top" Width="353"
  SelectedValuePath="Brush" DisplayMemberPath="Name"/>

 

第四版:Binding

WPF 特有的 Binding 可以取代一些 Event Handler,如同在WPF 學習:Binding中已經使用過這樣的技巧。因此, ColorListBox_SelectionChanged 這樣簡單的有一些可以移到 xaml 中。

<Window x:Class="WpfApplication2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525" Background="{Binding ElementName=ColorListBox, Path=SelectedValue}">

意思是:Window 的 Background 值繫結來至 ColorListBox 的 SelectedValue。

到了這個步驟,整個 Code behind 只剩下

using System.Windows;

namespace WpfApplication2
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            ColorListBox.ItemsSource = NamedBrush.All;
        }
    }
}

換句話說,只剩一行的 ColorListBox.ItemsItemsSource = NamedBrush.All; 這一行程式要寫。其他的都移到了 xaml中。

第五版:使用靜態 Binding

雖說已經簡化到了不行,剩下的最後一行,難到無計可施嗎?WPF 還有一招:StaticBinding。在 xaml 宣告一個 Window Resource 如下

<Window x:Class="WpfApplication2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:l="clr-namespace:WpfApplication2"
        Title="MainWindow" Height="350" Width="525" Background="{Binding ElementName=ColorListBox, Path=SelectedValue}">
    <Window.Resources>
        <l:NamedBrush x:Key="NB" />
    </Window.Resources>

注意到我增加了 xmaln:l=’clr-namespace:WpfApplication2’ 這一行 namespace 宣告。

最後,在 ColorListBox 的 ItemsSource 屬性設定如下

<ListBox Height="213" HorizontalAlignment="Left" Margin="76,12,0,0" Name="ColorListBox" VerticalAlignment="Top" Width="353"
                 SelectedValuePath="Brush" DisplayMemberPath="Name" 
                 ItemsSource="{Binding Source={StaticResource ResourceKey=NB}, Path=All}"/>

這樣一來,Code Behind 的程式就與最初的相同了。

using System.Windows;

namespace WpfApplication2
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }
}

討論

為什麼我們汲汲營營的要把 xaml.cs 的程式降到0行客製程式呢?第五版的步驟,為了將一行的建構子程式,我們修改了 xaml 三個地方,這樣划的來嗎?

其實,這樣做有兩大好處。第一項好處,是沒有 Code behind 的程式後,美工的部份可以完全交給設計人員。設計人員只需使用 Expression Blend 來設計樣式,完全不必懂程式如何寫作,就可以使用 Binding 的方式設計行為。

第二項好處,是UI 的資料(Model)完全分離,這有利於單元測試。對於容易進行測試的 NameBrush 類別進行單元測試,而難以進行測試,又常常改化的 UI就可以進行 Coded UI Test 了。

範例程式下載

2010年9月3日 星期五

WPF 應用程式,App.xaml 不使用 StartupUri 時無法套用 ResourceDictionary

前言

WPF 實在是很棒的一個 Framework,而且在開發設計時,Visual Studio IDE 的輔助,讓我們的開發更具生產力。但也因為如此,當我們不朝預設的開發方式時,就容易遇到一些「地雷」。今天我就踩到一個。

問題

問題的由來是有演變過程的。以下就一步步的演進吧。

步驟1: 標準的 Style

我按標準步驟產生了一個 WPF Application後,在 MainWindow.xaml  上放了一個 Button。

image

Xaml 如下, 非常簡單

<Window x:Class="WpfApplication2.MainWindow"
                xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                Title="MainWindow" Height="350" Width="525">
        <Grid>
        <Button Content="Button" Height="23" HorizontalAlignment="Left" Margin="40,36,0,0" Name="button1" VerticalAlignment="Top" Width="75" />
    </Grid>
</Window>

 

 

步驟2: 套用標準的 Style 到每一個 Button

為了讓整個程式的按鍵看來有一致的外觀,我在 App.xaml 上增加了 Button Style。App.xaml 如下,也非常的 easy

<Application x:Class="WpfApplication2.App"
                         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                         StartupUri="MainWindow.xaml">
        <Application.Resources>
        <ResourceDictionary>
            <Style TargetType="Button">
                <Setter Property="Background" Value="LightBlue" />
            </Style>
        </ResourceDictionary>
    </Application.Resources>
</Application>

而無論在開發階段或執行階段,Button 的樣式果然成我預期的背景顏色LightBlue 淺藍色。

image image

步驟3:移除 StartupUri

因故,我必須移除 App.xaml 中的 StartupUri,並自行建立 MainWindow 的 instance。修改後的 App.xaml如下

<Application x:Class="WpfApplication2.App"
                         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                         > <!--注意到我移除了 StartupUri-->
        <Application.Resources>
        <ResourceDictionary>
            <Style TargetType="Button">
                <Setter Property="Background" Value="LightBlue" />
            </Style>
        </ResourceDictionary>
    </Application.Resources>
</Application>

而 App.xaml.cs 如下

using System.Windows;

namespace WpfApplication2
{
    public partial class App : Application
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);
            var mainWindow = new MainWindow();
            mainWindow.Show();
        }
    }
}

程式可以執行,但問題來了:執行時期,Button 的樣式跑掉了。

image

這是怎麼一回事?開發的Designer 仍然是淺藍色背景--我要的樣式啊!

原因

原因是:開發工具太強了,幫我們做了太多的事。將原來的 StartupUri 加回去,並在 Solution Explorer 下找到 obj\Debug\ 可以找到一堆由 Visual Studio 幫我們產生的程式碼。

image

打開 App.g.i.cs ,可以找到 InitializeComponent 這個方法。可以看到它是如何幫我們自動載入 Resource 的.

image

一旦我們將 StartupUri 移除掉,這段程式就會變化。整個 InitializeComponent 方法就不見了!!

這就是原因所在。

解決

既然找到了原因,當然就知道要怎麼解了。方法有二。

第一個方法是自己補上消失的程式,並且自己將消失的 style 補上。這個方法難度較高,而且不好維護。我採用第二個方法:自訂一個 ResourceDictionary。

新增一個 ResourceDictionary 並命名為 AppResource.xaml, 並將原來在 App.xaml 的 style 移到這裡。

SNAGHTML8e2c6c

AppResource.xaml

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                                        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Style TargetType="Button">
        <Setter Property="Background" Value="LightBlue" />
    </Style>
</ResourceDictionary>

改用 。修改App.xaml 如下

<Application x:Class="WpfApplication2.App"
                         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                         > <!--注意到我移除了 StartupUri-->
        <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="AppResource.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

然後,執行時期的 Button 樣式就回來了。

image

結論

WPF 實在設計的很棒,而 IDE 的開發輔助也讓我們快速的完成常見的需求。只是在 IDE 快速開發的背後,我們常常忽略了原來應該是我們要做的事。一旦不符合原 IDE 的快速開發條件時,就摸不著頭緒了。

2010年8月10日 星期二

WPF 學習:透明應用程式

有些程式有獨特的風格,例如透明應用程式就是一例。那要如何寫出這類風格的程式呢?

Xaml

<Window x:Class="WpfApplication3.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525" AllowsTransparency="True" WindowStyle="None"
        Background="Transparent" MouseLeftButtonDown="Window_MouseLeftButtonDown">
    <Grid>
    <Ellipse Opacity="0.5" Fill="Green">
      <Ellipse.BitmapEffect>
        <DropShadowBitmapEffect />
      </Ellipse.BitmapEffect>
    </Ellipse>
  </Grid>
</Window>

重點
  1. Window 的 AllowsTransparency 必須設為 True,否則無法呈現透明。
  2. Window 的 WindowStyle 必須設為 None。否則會出現例外。
  3. Window 的 Background 必須設為 Transparent,以顯示透明效果。
經過這三個設定後,程式一跑,就看不到任何的程式內容了。所以我們必須加上內容。如上面範例所示,我增加了一個 半透明的Ellipse,並且以 DropShadowBitmapEffect 產生些微的陰影效果。 再來,為了讓應用程式的 Window 可以移動,我加了MouseLeftButtonDown 的事件處理器。
private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
  this.DragMove();
}
這樣,我們就可以拖動視窗了。範例執行畫面如下。

image

2010年7月29日 星期四

WPF 學習:EventRouting

這次學到的是 WPF 的 EventRouting

有一個 Window,xaml 如下

<Window x:Class="WpfApplication1.MainForm"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainForm" Height="300" Width="300" >
    <Grid>
        <StackPanel Name="MyPanel"></StackPanel>
    </Grid>
</Window>

而 code 如下。

public MainForm()
{
    InitializeComponent();

    for (int i = 0; i < 5; i++)
    {
        var button = new Button() {Content = "Button " + i};
        MyPanel.Children.Add(button);
        button.Click += new RoutedEventHandler(button_Click);
    }
}

void button_Click(object sender, RoutedEventArgs e)
{
    var button = sender as Button;
    MessageBox.Show(button.Content.ToString());
}

執行時,會產生5個 button。每一個 button 被Click 後,會拋出所click 的按鍵訊息。

這樣的寫作方式與以前的 windows form 並沒有什麼兩樣。每個 button 的 click event 都註冊了相同的 event handler,似乎有點笨。

Event Bubbling

由於 event bubbling 的關係,button 的也會傳到其 Parent Container,在這個例子是名為 MyPanel 的 StackPanel。因此理論上MyPanel 可以收的到 Click Event.

問題來了,StackPanel 並沒有 Click Event,因此無法註冊 EventHandler. 如下圖。

image

RountEvent

雖然 MyPanel 無法直接註冊 Click Event, 但在 WPF 中由於 DependecyProperty 的幫助,我們可以使用 MyPanel.AddHandler方法的協助來註冊 Button 的 Click Event 的 Event Handler

public MainForm()
{
    InitializeComponent();

    for (int i = 0; i < 5; i++)
    {
        var button = new Button() {Content = "Button " + i};
        MyPanel.Children.Add(button);
        //button.Click += new RoutedEventHandler(button_Click);
    }
    MyPanel.AddHandler(Button.ClickEvent, new RoutedEventHandler(button_Click));            
}

void button_Click(object sender, RoutedEventArgs e)
{
    var button = e.Source as Button; //我也改了這一行
    MessageBox.Show(button.Content.ToString());
}
注意到我改了 e.Source。e.Source 是指真正發生 Event 的物件,即 Button。而sender 會指到 MyPanel,並不是我們想要的。

WPF 學習:Binding

WPF 除了 Declaration Programming 外,還有一個最強的功能就屬於 Binding 了。

假設現在有個需求,有一個 Button 與 TextBox。TextBox 所輸入的文字必須即時地設定為 Button 的 Text。

Window Form

如果以 WinForm 來實作的話,就必須以 Event Handler 的方式來處理。程式碼如下.

private void textBox1_TextChanged(object sender, EventArgs e)
{
    button1.Text = textBox1.Text;
}
好處是相當簡易清楚。但想一想,程式中充滿了這類的 Event Handler 也是相當雜亂的事。

WPF

WPF 則是使用 Binding 的技術來克服這樣的需求,不必一項項的以 Event Handler 來實作。

<Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
  <Button    Content="{Binding ElementName=HelloTextBox, Path=Text}" Height="100" Margin="0,12,0,198" />
  <Button    Height="100" Margin="0,128,0,83" >
    <Button.Content>
    <Binding ElementName="HelloTextBox" Path="Text" />
    </Button.Content>
  </Button>
  <TextBox Text="Hello" Name="HelloTextBox" Margin="0,234,374,49" />
</Grid>
</Window>

第一個 Button ,其 Content 說明了其資料來源的Element 為 HelloTextBox,讀取路徑(Path)為Text。

第二個 Button 則使用了另一種相等的寫法,更清楚但語法稍嫌太長。

結論

這兩種WPF 的方法使用了 Binding(繫結),簡單地以宣告的方式取代了 Event Handling。更棒的是,這類的 Binding 可以使用到非常多的屬性,如Style, Icon 等。

Share with Facebook