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);
}