虽然才过了一天,但是我已经不记得之前做了什么了(误。
在完成主界面之后,就是开始对各个页面进行编辑了。主界面我选择的是用TabControl 来实现,所以可以将每个界面的元素都写在不同的TabItem 中。
还是先写数据绑定,再来说UI 的部分吧。绑定就先用最简单的表Building 来做例子。
首先,打开SSMS,新建几个存储过程。
USE EducationManager GO CREATE PROCEDURE GetBuildings AS BEGIN SELECT * FROM Building ORDER BY 2, 1; END GO CREATE PROCEDURE AddBuilding(@bldgName nvarchar(6), @roomNum int) AS BEGIN INSERT INTO Building VALUES ( @bldgName, @roomNum); END GO CREATE PROCEDURE DeleteBuilding(@bldgName nvarchar(6), @roomNum int) AS BEGIN DELETE FROM Building WHERE BldgName = @bldgName AND RoomNum = @roomNum; END GO
分别是获取所有行,增加以及删除行。
回到VS,在EducationManagerClass 项目中,新建一个文件夹命名为Models,然后在里面添加一个类Building。
1 public class Building 2 { 3 public string BldgName { get; set; } 4 5 public int RoomNum { get; set; } 6 7 public Building(string bldgName, int roomNum) 8 { 9 BldgName = bldgName; 10 RoomNum = roomNum; 11 } 12 13 public override string ToString() 14 { 15 return BldgName + " " + RoomNum; 16 } 17 }
注意要重写ToString() 方法,不然是显示不出来数据的。这个类就是表Building 在整改项目中的模型类。
然后打开AppDbConnection 类,由于这些存储过程在每次使用时,都要将类中的所有属性设为参数,所以我又顺便写了个帮助方法SetSqlParamters,来将模型类中所有的属性添加到SqlCommand 的参数中。这个方法的具体解释可以看【乱写代码坑人系列】小插曲(一)。
1 // 通过反射自动设置参数值 2 private void SetSqlParameters<T>(SqlCommand cmd, T model) 3 where T : class 4 { 5 foreach (PropertyInfo prop in 6 model.GetType().GetProperties 7 (BindingFlags.Instance | BindingFlags.Public)) 8 { 9 cmd.Parameters.AddWithValue( 10 "@" + prop.Name, prop.GetValue(model, null)); 11 } 12 }
然后添加对应这三个存储过程的方法,都是使用了异步的方法来执行Sql 的操作,不过在这里遇到了一个问题,就是在使用OpenAsync 方法的时候,会突然就不知道跳到哪里去了,UI也出不来,但也没报异常……有知道的还麻烦告诉一下,感激不尽。Add 和Delete 两个方法都会返回有多少行受到了影响,虽然没什么用但是我依旧返回了,之后若是遇到有返回值的也会进行返回。
1 public async Task<ICollection<Building>> GetBuildings() 2 { 3 using (SqlConnection con = new SqlConnection(AppConnectionString)) 4 { 5 SqlCommand cmd = new SqlCommand("GetBuildings", con); 6 cmd.CommandType = CommandType.StoredProcedure; 7 ObservableCollection<Building> buildings = new ObservableCollection<Building>(); 8 9 // 由于是在using 语句块中,所以就算抛了异常,con 也会自然阵亡,不用手动Close() 或Dispose() 10 con.Open(); 11 SqlDataReader reader = await cmd.ExecuteReaderAsync(); 12 while (reader.Read()) 13 { 14 Building building = new Building 15 ( 16 (string)reader["BldgName"], 17 (int)reader["RoomNum"] 18 ); 19 buildings.Add(building); 20 } 21 reader.Close(); 22 23 return buildings; 24 } 25 } 26 27 public async Task<int> AddBuilding(Building building) 28 { 29 using (SqlConnection con = new SqlConnection(AppConnectionString)) 30 { 31 SqlCommand cmd = new SqlCommand("AddBuilding", con); 32 cmd.CommandType = CommandType.StoredProcedure; 33 SetSqlParameters(cmd, building); 34 35 con.Open(); 36 var lines = await cmd.ExecuteNonQueryAsync(); 37 return lines; 38 } 39 } 40 41 public async Task<int> DeleteBuilding(Building building) 42 { 43 using (SqlConnection con = new SqlConnection(AppConnectionString)) 44 { 45 SqlCommand cmd = new SqlCommand("DeleteBuilding", con); 46 cmd.CommandType = CommandType.StoredProcedure; 47 SetSqlParameters(cmd, building); 48 49 con.Open(); 50 var lines = await cmd.ExecuteNonQueryAsync(); 51 return lines; 52 } 53 }
到这里,对数据库进行操作的方法都写完了,也十分简单,似乎也没什么需要解释的。
然后到UI 部分。
首先,我们需要能够查看所有的数据,同时,如果只是简单的用ListView 来将所有的教室列出来的话是很不友好的,所以需要使用的是TreeView;我们还需要一个操作区来放一些按钮和输入框,来对数据进行操作。
选中教室管理的标签页,在上面添加一个Grid 并分成左右两半部分,左边放TreeView,右边就是操作区。
把新建和详情分开,确保它们之间不会相互干扰。同时要在xaml 代码中加入TreeView 的模板,来保证能正确的分组显示数据。
1 <Window.Resources> 2 <HierarchicalDataTemplate x:Key="tvit" ItemsSource="{Binding}"> 3 <TextBlock Text="{Binding Key}"/> 4 <HierarchicalDataTemplate.ItemTemplate> 5 <DataTemplate> 6 <TextBlock Text="{Binding}"/> 7 </DataTemplate> 8 </HierarchicalDataTemplate.ItemTemplate> 9 </HierarchicalDataTemplate> 10 </Window.Resources> 11 12 <!--Other code--> 13 14 <TreeView Name="buildingTree" Grid.Column="0" 15 ItemTemplate="{StaticResource ResourceKey=tvit}" > 16 </TreeView>
还有,详情框中的两个TextBox 需要进行数据绑定,将Binding 的Path 设为Building 类中对应的属性名,才能自动的显示对应信息。
<Label Content="建筑名" HorizontalAlignment="Left" Width="70" Height="25" VerticalAlignment="Top" Margin="0,38,0,0"/> <TextBox Name="bldgNameTextBox" Margin="75,38,10,0" Text="{Binding Path=BldgName}" Height="25" VerticalAlignment="Top"></TextBox> <Label Content="教室编号" Margin="0,68,0,0" HorizontalAlignment="Left" Width="70" Height="28" VerticalAlignment="Top"/> <TextBox Name="roomNumTextBox" Margin="75,68,10,0" Text="{Binding Path=RoomNum}" Height="28" VerticalAlignment="Top"></TextBox>
进入后台代码。
写之前,如果使用的是VS 2015,可以在最开始加上一句using static EductionManagerUI.App; ,这样就能直接使用App 类中的AppDbConnection 对象,而不用在前面加App. 了。
在程序启动时需要加载数据,而加载数据的方法都是异步的,在构造函数前也无法添加async 关键字,所以需要单独写一个初始化数据的方法。这个方法的返回值需要为async void 而不是Task,否则会在调用的地方(构造函数中)出现警告。由于返回值是void,所以也必须在这个方法中对异常进行捕获。同时,还需要一块地盘来保存从数据库获取的信息,以方便之后进行操作和处理。
1 // 存放Building 的属性 2 private ICollection<Building> Buildings { get; set; } 3 4 /// <summary> 5 /// 初始化数据,使用的是async void,所以必须使用catch 6 /// </summary> 7 private async void InitData() 8 { 9 try 10 { 11 Buildings = await AppDbconnection.GetBuildings(); 12 // Other initialization ... 13 } 14 catch 15 { 16 MessageBox.Show("数据初始化异常"); 17 Application.Current.Shutdown(); 18 } 19 buildingTree.ItemsSource = Buildings.GroupBy(b => b.BldgName); 20 }
注意,TreeView 的ItemsSource 的值应该是对Buildings 进行分组之后的结果,这样才能正确的分层显示。
然后开始写UI 元素的事件。
事件主要有这么几个:选择TreeView 的SelectedItemChanged事件,用来在选中的行改变时显示对应的信息;添加、删除按钮的Click 事件,这两个就是分别调用AppDbConnection 中对应的方法,并且对UI 进行更新;还有一个reset 按钮的Click 事件,就是在点击后清除详情框中的值。
另外,在事件中使用的也是async void ,而不是Task,这是没得选的,我也想做个好人,但是法官不让(逃。
先看看TreeView 的事件,在更新值时必须先判断选中的是不是子元素,如果是的话才进行显示,不然就假装四处看风景。
1 private void buildingTree_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<Object> e) 2 { 3 if (buildingTree.SelectedValue.GetType() == typeof(Building)) 4 { 5 var building = (Building)buildingTree.SelectedItem; 6 try 7 { 8 gridBuildingDetails.DataContext = building; 9 } 10 catch 11 { 12 MessageBox.Show("出现了奇怪的错误耶"); 13 } 14 } 15 }
异常的catch 部分可以改成输出log,我就是懒得写了所以直接用MessageBox了(……
添加和删除按钮的事件比较类似,只是数据的来源不同;在修改之后,可以用两个方法返回的受影响行数来判断是否成功,不过这里也没太大必要所以我就偷懒了啊哈哈,同时最好重新获取一遍数据库中的数据,并且必须重新设置TreeView 的ItemsSource 来刷新界面。这里应该是有办法实现无刷新更新的,不过感觉有点麻烦就没做,思路大概就是将Buildings 的类型改成分组后的类型,不过这样的话更新删除什么的都有点麻烦。而reset 按钮的事件只需要将绑定的信息设为null,就会自动清除了。
1 private async void bldgAddButton_Click(object sender, RoutedEventArgs e) 2 { 3 int roomNum; 4 if (int.TryParse(newRoomNumTextBox.Text, out roomNum)) 5 { 6 var building = new Building(newBldgNameTextBox.Text, roomNum); 7 try 8 { 9 await AppDbconnection.AddBuilding(building); 10 Buildings = await AppDbconnection.GetBuildings(); 11 buildingTree.ItemsSource = Buildings.GroupBy(b => b.BldgName); 12 } 13 catch 14 { 15 MessageBox.Show("添加失败"); 16 } 17 } 18 else 19 { 20 MessageBox.Show("教室编号有误"); 21 } 22 } 23 24 private async void bldgDelButton_Click(object sender, RoutedEventArgs e) 25 { 26 var building = (Building)gridBuildingDetails.DataContext; 27 if (building == null) 28 return; 29 30 try 31 { 32 await AppDbconnection.DeleteBuilding(building); 33 Buildings.Remove(building); 34 buildingTree.ItemsSource = Buildings.GroupBy(b => b.BldgName); 35 gridBuildingDetails.DataContext = null; 36 } 37 catch 38 { 39 MessageBox.Show("删除失败"); 40 } 41 } 42 43 private void bldgResetButton_Click(object sender, RoutedEventArgs e) 44 { 45 gridBuildingDetails.DataContext = null; 46 }
这里面的catch 也是可以加log 输出的,个人推荐这么做,而不是像这样只是一个消息框(严肃脸。
嗯,可以用了耶!
说实话晚上敲键盘这么响好怕被舍友砍。
嗯最后上Git:Github\LzxHahaha\EducationManager
转载请注明出处:LzxHahaha-博客园