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 來的直覺與方便。

沒有留言:

Share with Facebook