緣由
這個標題下的有些古怪。
我的專案,原本都以 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…等等。
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 間接取得資料。
結論
這個範例中,主要演示了 ASP.NET WebForm 與 WPF 的先天上的差異(Stateless vs Stateful),並且由於 WPF 雙向繫結的功能,使用者的輸入可以直接更新到資料來源,遠較WebForm 來的直覺與方便。