WPF入門教程系列(一) 創建你的第一個WPF項目
WPF基礎知識
快速學習絕不是從零學起的,良好的基礎是快速入手的關鍵,下面先為大家摞列以下自己總結的學習WPF的幾點基礎知識:
1) C#基礎語法知識(或者其他.NET支持的語言):這個是當然的了,雖然WPF是XAML配置的,但是總還是要寫代碼的,相信各位讀者應該也都有這個基礎了。
2) HTML語言:雖然WPF是窗體程序但是由於使用的XAML語言,如果以前接觸過HTML、XHTML、ASP.NET之路的東西的話會,接受這些標簽會很有幫助的,如果以前一直是從事win form開的人來說可能就要適應一下了。
3) 對C#中的代理、事件要做到熟練掌握,在.NET 3.0 版本后有增加了Routed Events,要想日后不糊塗,這個是基礎。
4) 有一定的winform或ASP.NET經驗,主要是對控件事件的處理要有寫了解。
5) 擁有良好的面向對象的思想:思想是語言的升華(本人的OO思想完全是Java中領悟來的)。在WPF中,經常要靈活運用各種繼承關系、多態、重載等,因此一定要把基礎知識打牢固。
6) DataBinding要有所了解:Binding是WPF的一大亮點,在接觸它以前如果接觸國ADO.NET里面的DataBinding的話對相對起來會容易接受一點,雖然這兩個有一定的不同。
7) 對設計模式要有一定的了解:當然是越深入越好了,在實際項目中,各種設計模式經常交融使用。
快速的識別並合理的運用,無論是在開發還是調試時都是非常高效的。另外,WPF存在的初衷即是表現與邏輯的松耦合,最普遍的情況就是XAML作為表現層,背后.cs文件作為邏輯層。因此,日后在從事項目工作時,要時刻謹記這一點,千萬不可背道而馳。
不要為了凸顯自己的某一些代碼特長而將各種邏輯混寫在一起,這樣非常不實際的,這一點都在校生應該尤為重要。
8) 對XML的理解:XAML也是XML,對XML的理解絕對有助於快速的接受和使用XAML,並不需要多XML有多么高深的見解。
但是最好還是有空看一看XML相關的書籍http://www.w3.org/TR/2000/WD-xml-2e-20000814。無論是WPF、WCP以及底層一些的SOAP等都是很有幫助的。
WPF入手練習基礎環境
開發環境:VS 2008
數據庫:本機SQL Server 2005(這里給大家一個提醒,如果大家的機子是Windows XP的話,無論是home 或是 professional.
一定不要安裝SQL Server 2005 Enterprise Edition,在《安裝 SQL Server 2005 的硬件和軟件要求》中“操作系統要求”列表有寫到2005 Enterprise Edition 不支持 XP),建議安裝Developer Edition,一定要安裝數據庫實例。
WPF練習內容
具備以上基礎條件后,開始入手練習,大家不要抱怨入手練習有寫難度,畢竟是快速入門要有一定的跳躍性:
我們要做一個WPF程序,功能很簡單:
1) 從數據庫(本地數據庫(local)/AdventureWorks中的person.contact表中提取用戶的ContactID,FirstName,LastName,EmailAddress數據,展示到Form上的一個ListView上。(由於是WPF練習,對於ADO.NET相關的東西在此不做介紹,知識使用而已)
2) 當鼠標或其他設備選中結果某一項記錄時,在List框下面展示出細節。
3) 修改其中的內容后,結果聯動更新到List框及數據庫中。
想象以下這樣一個東西如果在以前使用winform實現會是什么樣子呢?
應該會寫不少的方法、屬性用於界面之間及界面與數據庫之間的聯動。
今天這個練習就先展示以下WPF的技術亮點之一:
DataBinding。在制作過程中,還會為大家不斷接受一些控件、布局等相關知識和技巧,部分相關的知識內容與此練習關系不大的,我將會用淺灰色字體帶過,對於熟悉這部分內容的讀者可以直接跳過。
建立一個WPF項目
打開VS 2008 新建一個WPF應用程序
WPF界面布局
首先會看到一個靚麗的小方框,將鼠標放在方框的邊緣點擊就會產生相應的分割線。
現在我們要做的內容需要將窗體分成三行,可以先隨便分割一下,以后在調整相互的大小。這時候會注意到下方的XML代碼區域。每個RowDefinition作為一個行被定義出來
這里先給大家接受以下高度、寬度的幾種定義方式(寫過HTML的人可以跳過去了):
絕對尺寸(Absolut sizing):就是給一個實際的數字,像現在例子中那樣
自動(Autosizing):值為Auto,實際作用就是取實際控件所需的最小值(Setting Height or Width to Auto, which gives child elements the space that they need and no more)。
Proportional sizing(也可以稱之為star sizing因為有個*號表示):
值為*或N*,實際作用就是取盡可能大的值,當某一列或行被定義為*則是盡可能大,當出現多列或行被定義為*則是代表幾者之間按比例方設置尺寸。
下面我們來將上面的知識應用以下看看。
將這三行的值設置為如下數值,結果會是如何:
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="22" />
第0行設為*用來放置ListView,這樣會在實際運行過程中盡可能的充滿整個區域
第1行設為Auto盡量緊湊配列,少占用空間(如果里面沒有東西的話,他會小到0,這是你會看不到它)
第2行設為22固定值,只是用來放一個Button使用
對於初學者往往會習慣直接使用控件拖拽的形式來將需要的內容添加到窗體上。
會使控件在一定的坐標上固定位置,這是一種不推薦的做法,在此給大家講解以下WPF中的布局觀(Layout Philosophy):
在WPF窗體中,一個窗體只能持有一個控件,當多個控件想要在窗體中展現時,就需要首先設置一個容器控件(Container)
然后將其他控件放到這個控件里面,形成樹狀結構.
因此,布局觀第一條就是,控件的布局應該有容器來決定,而不是通過自身使用margin之類的東西來控制位置。
因為這些屬性原本應該是控制自己內部展現或與鄰里之間關系的;
第二條,控件應避免明確的定義具體的尺寸,因為顯示器分辨率及windows窗體的大小都有可能隨時改變,如果明確的定義尺寸。
當窗體變動后就會出現大面積的空白或是缺失。但為了控件功能及效果的展示,應該限定一個可接受的最大及最小尺寸。
通過MinWidth, MinHeight, MaxWidth, MaxHeight屬性可以實現這一點。第三條,不要將界面元素位置設置成與屏幕坐標相關.
現在顯示器分辨率比較多樣話(800×600、1024×768,我的顯示器是一台是1400×1050,還有一個是1024×1280豎式的),這樣的做法還是比較有風險的。
第四條,容器應將有效空間共享給其子控件,這也是為了不在窗體調整后,遺留出大塊的空余。
第五條,容器嵌套使用,因為不同的容器,表現效果不同,必要時應結合使用。
接下來在工具箱(Tool Box)中雙擊ListView,一個小框會出現在界面上。
接下來在工具箱(Tool Box)中雙擊WrapPanel,又一個大框會出現在界面上。
再增加一個Button。
這是你會感覺到界面有點亂了,剛才我們在頂層Grid上面畫線到底起什么作用了(到目前位置還沒有),讓我們來調整一下下面的XAML語句,最終結果如下:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="22" />
</Grid.RowDefinitions>
<ListView Name="listView1" MinWidth="280" >
<ListView.View>
<GridView x:Name="gridView1">
<GridViewColumn Header="ContactID"></GridViewColumn>
<GridViewColumn Header="FirstName"></GridViewColumn>
<GridViewColumn Header="LastName"></GridViewColumn>
<GridViewColumn Header="EmailAddress"></GridViewColumn>
</GridView>
</ListView.View>
</ListView>
<WrapPanel Grid.Row="1" Orientation="Horizontal"></WrapPanel>
<Button Grid.Row="2" HorizontalAlignment="Right" Click="button1_Click" Name="button1">Refresh</Button>
</Grid>
這里有幾點又需要解釋以下了:
1) 介紹以下容器控件Panel,現在界面中有兩個容器型的控件一個是Grid跟元素,另一個是WrapPanel。它們都是容器型控件,不過表現上有所不同。
Grid顧名思義“網格”,在之前我們已經定義了三行高度分辨是*, Auto, 22,其實還可以功過ColumnDefinition定義多個列,他的子控件被放在一個一個實現定義好的小格子里面,整齊配列。
而WrapPanel則是將各個控件按照行或列的順序摞列,當長度或高度不夠是就會自動調整換例或行。
還有一個常用控件這里稍后會用到StackPanel,將控件按照行或列來順序排列不會回行。
2) 大家應該注意到了在WrapPanel及Button上面的Grid.Row="n",這個就是Attached Properties(不知道怎么翻譯了,可能叫‘附着屬性’).
用來設置WrapPanel及Button應該在父容器的什么位置。這是WPF的特性之一,通俗的理解起來就是,別人有的屬性,由於你跟他產生了關系所以你也有了這個屬於他的屬性。
記得FantasiaX‘水之真諦’曾經給我通俗的解釋過這個特性,這里照搬出來分享給大家:一個小學生,身高、體重是他的自身屬性,而這個小學生由於是N年級的X班的學生.
因此,這個小學生又帶有了一個附加的屬性,N年級X班。在這個例子里如果學校作為一個Grid容器,N年級X班可以看作一個小格子,小學生是其中的一個實例,那么,小學生因為安置在這個班級.
因此獲得了這個班級所擁有的這個屬性,當學期末,老師說,這個學生已經升到N+1年級X班時,這個學生以后就跑到另一個小格子里去上課了。
Attached Properties的XAML用法就是在自己的屬性設置地方直接使用容器的類型名稱.容器屬性名稱(Grid.Row)設置對應的值。
3) 大家應該注意到類似與ListView.View及Grid.RowDefinitions用法,這個叫做Complex Properties(應該叫‘復雜屬性’吧).
其實就是元素的某一個屬性由於不能夠簡單的用名值對實現,因此需要單獨標簽話聲明一下。
4) 再有就是x:Name="gridView1"這種用法,叫做Markup Extensions(‘標記擴展’吧),這個可是一個滿不錯的特性.
由於后面代碼中要使用到GridView對象,然而GridView對象有沒有Name屬性,如果后台代碼想要調用他的話就不得不從父容器向下遍歷來找到想要的對象,這樣無疑增加了后台代碼與前台界面之間的耦合度,試想如果那天突然有需求說要把這個對象從這里移到另一個容器上去.
那么界面的變動伴隨着的就是后台代碼的一起變動,這與視圖/邏輯分離顯然背道而馳,有了Markup Extensions.
我們想定位一個沒有名字屬性的控件,直接為擴展一個名稱出來,這個可太方便了(當然,Markup Extensions不只是用來擴展名稱的)。
如果希望每個TextBlock和TextBox成為一對出現的話,應該如何呢?
自然是需要一個容器將他們組織起來.
同時,希望他們在一條線上不回行。這就用到了,我們前面說到的一個容器StackPanel。組織后的代碼如下:
<WrapPanel Grid.Row="1" Orientation="Horizontal">
<StackPanel Orientation="Horizontal" Margin="5,2,5,2">
<TextBlock Name="textBlock_ContactID" Text="ContactID:" />
<TextBox Name="textBox_ContactID" MinWidth="100" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="5,2,5,2">
<TextBlock Name="textBlock_FirstName" Text="FirstName:" />
<TextBox Name="textBox_FirstName" MinWidth="100" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="5,2,5,2">
<TextBlock Name="textBlock_LastName" Text="LastName:" />
<TextBox Name="textBox_LastName" MinWidth="100" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="5,2,5,2">
<TextBlock Name="textBlock_EmailAddress" Text="EmailAddress:" />
<TextBox Name="textBox_EmailAddress" MinWidth="100" />
</StackPanel>
</WrapPanel>
這樣情況下在試試就可以看到效果了,無論窗體邊緣怎么托拉TextBlock和TextBox總是成對的.
同時,隨着窗體的拖動,控件會不斷的改變位置一適應最小原則(如果想要讓他固定下來的話.
那就需要將WrapPanel換成其他的Panel就可以了)。如下圖所示:
補充知識:
WPF中所有單一內容控件的統稱,對應的父類為System.Windows.Controls.ContentControl;
與ContentControl平行的還有一類控件叫做 ItemsControl,WPF中所有控件集合對象的統稱,例如:ListView, ListBox, TreeView等,這一點一定要區分清楚.
可以認為這一類控件是為了展現有效的展現及組織一組控件而設計的,對應的父類為System.Windows.Controls.ItemsControl,這些控件和Panel容器類控件是不同的概念,要了解他們之間的關系可以參看下面這張類繼承關系圖:
WPF后台邏輯代碼編寫
在后台用ADO.NET寫一個獲取DataTable的方法及一個共有屬性擁有獲取DataTable.DefaultView.
同時,實現Refresh按鈕的方法。此部分不是練習重點這里不做介紹了,代碼如下:
public Window2()
{
InitializeComponent();
getData();
}
SqlDataAdapter sda;
DataTable dt;
void getData()
{
//init sqlconnection
SqlConnectionStringBuilder connbuilder = new SqlConnectionStringBuilder();
connbuilder.DataSource = "(local)";
connbuilder.IntegratedSecurity = true;
connbuilder.InitialCatalog = "AdventureWorks";
//start to make sql query
SqlConnection conn = new SqlConnection(connbuilder.ConnectionString);
sda = new SqlDataAdapter("select ContactID,FirstName,LastName,EmailAddress from person.contact where ContactID<=100;", conn);
SqlCommandBuilder commbuilder = new SqlCommandBuilder(sda);
sda.UpdateCommand = commbuilder.GetUpdateCommand();
dt = new DataTable();
sda.AcceptChangesDuringUpdate = true;
sda.Fill(dt);
}
private void button1_Click(object sender, RoutedEventArgs e)
{
getData();
}
WPF前台界面與后台數據的Binding
Binding這個此翻譯起來有爭議,為了避免誤導大家,因此我還是不寫成中文了。
好了!接下來終於到最精彩的地方了。各位讀者們一會會為后面所發生的事情而感到震驚的,一切就是這樣的完成了……
1) 為ListView控件指明數據源,通過ItemSource屬性設置,這一部分需要通過代碼來實現(目前位置還不知道如何通過XAML語言來實現,哪位大哥可以指點一下)。
在void getData()方法的結尾處增加一行代碼“listView1.ItemsSource = dt.DefaultView;”
2) 給GridViewColumn指明當前列對應於數據源的哪一項,可以通過DisplayMemberBinding屬性來實現.
其中的值便是上一步中指明的數據源dt.DefaultView的每一個數據項的名稱.
<GridViewColumn Header="ContactID" DisplayMemberBinding="{Binding Path=ContactID}"></GridViewColumn>
<GridViewColumn Header="FirstName" DisplayMemberBinding="{Binding Path=FirstName}"></GridViewColumn>
<GridViewColumn Header="LastName" DisplayMemberBinding="{Binding Path=LastName}"></GridViewColumn>
<GridViewColumn Header="EmailAddress" DisplayMemberBinding="{Binding Path=EmailAddress}"></GridViewColumn>
此時試着F5 Debug運行以下,應該已經可以看到這樣的畫面了
但是,當我們點擊ListView中的記錄時,里面的書籍並沒有映射到下面的文本框中。
接下來我們就來實現這部分功能。
1) 為了簡化代碼,在WrapPanel元素中指明一個公共的上下文,可以通過增加屬性
DataContext="{Binding ElementName=listView1,Path=SelectedItem}
2) 分別為四個輸入框指明相應的數據源,可以通過TextBox元素中的Text屬性實現,實現后代碼如下:
<TextBox Name="textBox_ContactID" MinWidth="100" Text="{Binding ContactID}" />
……
<TextBox Name="textBox_FirstName" MinWidth="100" Text="{Binding FirstName,UpdateSourceTrigger=PropertyChanged }" />
……
<TextBox Name="textBox_LastName" MinWidth="100" Text="{Binding LastName}" />
……
<TextBox Name="textBox_EmailAddress" MinWidth="100" Text="{Binding EmailAddress}" />
上面這一段代碼相信有很多剛剛接觸WPF的人一定會感到陌生,這里解釋一下:
上面這段XAML語句里面,大家看着最不順眼的應該就是{Binding …}這種語句了吧,這個就是WPF重要特性之一“Binding”。
他是用來實現界面元素的屬性與后台數據之間的Binding,通過這種形式將前台界面與后台數據聯系在一起達到界面與數據耦合的目的。
與直接覆值相比較,存在這如下幾點特性上的差異:
1) Binding可以通過XAML語句實現界面與數據(可以是界面元素或后台對象)的耦合(也可以通過代碼來實現)。
這一實現主要是依靠WPF的另一個特性Dependency Property來實現的。示意圖如下:
2) Binding可以實現制定方向的綁定,方向有三種,OneWay, TwoWay, OneWaytoSource,其形象的表示如下圖所示:
3) 可配置觸發器,這一特性用來解釋,界面與數據的Binding是什么時候發生的,可以通過UpdateSourceTrigger屬性實現,存在如下幾種值
LostFocus :當控件失去焦點時觸發,前面例子里
TextBox.Text默認就是這種形式的
PropertyChanged:當屬性改變時觸發
Explicit:這個就可以看作是需要顯示調用了,需要主動取調用相應的UpdateSource方法才可以觸發
用法可以像這樣“{Binding FirstName,UpdateSourceTrigger=PropertyChanged }”
4) 不拋出異常,這一點對於開發及測試人員來說可能並不怎么好。
當一個數據Binding失敗是,程序運行是不受影響的,只是相應的屬性值為空了,對於開發人員來講只能通過VS Debug時的輸出窗口看到Binding失敗的調試信息。
而對於測試人員來說那就只能是靠肉眼了。
具體內容可以參考MSDN中的” Data Binding Overview”這篇文章,這里只是為大家實現一個引路的工作
現在再試試應該已經可以實現ListView與TextBox的聯動了,不過此時,對數據的改動還無法更新到數據庫中。
需要繼續修改。
1) 在button1_Click事件中增加一條語句用於接受書籍更新sda.Update(dt);
2) 在XAML設計區,最頂層Window元素中增加屬性,Closed="Window_Closed",然后在后台實現相應的方法代碼如下
兩個方法具體代碼如下,由於與WPF關系不大,因此不做講解了:
private void button1_Click(object sender, RoutedEventArgs e)
{
sda.Update(dt);
getData();
}
private void Window_Closed(object sender, EventArgs e)
{
sda.Update(dt);
}