使用 WPF MVVM 进行数据库访问






4.97/5 (30投票s)
一个使用WPF MVVM访问简单遗留数据库的示例。
引言
我喜欢将有用的代码示例放在手边,因为它们在新项目开始时可能非常宝贵。我保留的一个示例是一个包含几个表和存储过程的微型数据库,以及一个相应的WPF实用程序来访问该数据库。该示例非常标准,直接WPF通过SQLCommand
调用DB过程。随着时间的推移,我意识到必须更新该示例以反映当前的编码实践。具体来说,我希望WPF实用程序使用MVVM模式,并采用LINQ to SQL来访问数据库。数据库本身没有改变。本文描述了生成的代码,需要添加的内容以及途中遇到的惊喜。
考虑到很少有人会对需要安装数据库的代码感兴趣,我试图通过演示一些不相关的功能来使其更具吸引力。这包括凝胶按钮和无缝重复的背景图块。这是我的第一个MVVM尝试。请告诉我我的表现如何。
入门
数据库xstoredb必须安装。下载源代码或可执行文件。在顶层目录中有一个名为CreateStoreDb.sql的文件。用编辑器打开此文件并将内容复制到剪贴板。运行SQL Server Management Studio并打开一个新的查询窗口。将剪贴板上的代码粘贴到查询窗口中,然后单击“执行”按钮。这将创建一个未填充数据的名为xstoredb的数据库。该数据库很简单,只有两个小表和四个存储过程。但是,它提供了生产数据库中存在的功能。
安装xstoredb数据库后,您应该知道连接到它的连接字符串。此连接字符串必须提供给包含的ProductMvvm实用程序,以便它能够成功连接到xstoredb。提供正确的连接字符串可能会令人沮丧。在开始之前,您应该对字符串的外观有一个很好的了解。连接字符串通过应用程序配置文件提供给ProductMvvm。打开ProductMvvm.exe.config,您将看到连接字符串位于以下XAML中...
<connectionStrings>
<add name="ProductMvvm.Properties.Settings.xstoredbConnectionString"
connectionString="Data Source=DOUG-PC;Initial Catalog=xstoredb;Integrated Security=True"
providerName="System.Data.SqlClient" />
</connectionStrings>
修改第10行上connectionString
属性的值,以指定访问您安装的xstoredb数据库所需的连接字符串。上面显示的值指定了我的家庭PC(DOUG-PC)使用Windows集成身份验证。配置文件中只需修改一行。一旦提供了有效的连接字符串,ProductMvvm实用程序就会运行。
运行ProductMvvm.exe来测试连接字符串。由于数据库最初是空的,因此不会显示任何产品。可以通过单击“DB Refresh”按钮来检查数据库连接。状态字段应显示“OK”,表明实用程序能够访问数据库。填写产品字段并使用“Add”按钮在数据库中创建产品。下方显示了添加几个产品后的显示快照。操作员正要删除显示顶部蓝色产品选择器屏幕中选择的第三个产品。
关于显示
<Window x:Class="ProductMvvm.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vw="clr-namespace:ProductMvvm.Views"
Title="ProductMvvm" Height="550" Width="370" MinHeight="550"
WindowStartupLocation="CenterScreen" Loaded="Window_Loaded">
<Grid>
<Grid.Background>
<ImageBrush ImageSource="LightBrushedx.jpg" TileMode="Tile"
ViewportUnits="Absolute" Viewport="0,0,200,200">
</ImageBrush>
</Grid.Background>
<Grid.RowDefinitions>
<RowDefinition Height="3*"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="7*"></RowDefinition>
</Grid.RowDefinitions>
<vw:ProductSelectionView Grid.Row="0"/>
<GridSplitter Grid.Row="1" HorizontalAlignment="Stretch"
VerticalAlignment="Bottom" ResizeBehavior="PreviousAndNext"
Height="5"/>
<vw:ProductDisplay Grid.Row="2"/>
</Grid>
</Window>
检查Window1.xaml文件会发现一个简单的网格,包含三行。这些行包含一个ProductSelectionView
、一个GridSplitter
和一个ProductDisplayView
。注意为视图定义的vw
命名空间及其用于引用这些视图类的用法。使用两个视图是为了使代码保持合理的简单性,但允许在解耦的方式下说明多个视图之间的交互。Window_Loaded
事件只是一个占位符,在代码中未使用。另请注意使用LightBrushedx.jpg作为网格的重复背景图块。检查显示以验证图块确实是无缝的。读者将不得不原谅我的艺术性。我试图达到拉丝金属的外观。然而,这个图块足够有趣,可以提供潜力。
ProductSelectionView
是一个列表框,显示所有产品的型号名称。在数据库中,产品的ModelName被定义为唯一的。操作员通过单击ProductSelectionView
中的型号名称来选择单个产品。可以使用控件单击来取消选择产品。选择的存在或不存在决定了允许哪些数据库操作。要更新或删除产品,需要进行选择。添加新产品时,不允许进行选择。上面的图像显示了正在删除的产品。注意:有一个选定的产品。“Add”按钮被禁用,因为存在一个活动选择。
ProductDisplayView
是更复杂的视图。它包含大量的功能,以将此示例限制为两个视图。ProductDisplayView
包含访问数据库的命令按钮、显示选定产品的文本字段以及显示数据库状态或错误消息的状态字段。显示产品的文本字段可以显示 WPF的错误检查。实际上,并未采用WPF的错误检查功能。使用自制错误检查以尽量减少操作员的干扰。操作员在修改产品文本字段时不会对其进行检查。错误检查是在最后可能的时刻执行的,即当操作员单击某个按钮时。单击的具体按钮决定了是否对任何字段进行错误检查。例如,删除只需确保已选择一个产品。由于字段的内容对于请求的操作的成功无关紧要,因此不对删除执行字段错误检查。
下面简要描述了命令按钮...
- DB Refresh - 重新启动实用程序。再次访问数据库以获取所有当前产品。使用此按钮确认产品修改已正确输入数据库。
- Clear - 一个便捷功能。取消选择任何选定的产品,并清除所有产品文本字段。
- Update - 使用产品文本字段的内容在数据库中更新选定的产品。
- Delete - 从数据库中删除选定的产品。
- Add - 使用产品文本字段的内容在数据库中创建新产品。
视图
MVVM视图通常避免使用命名元素,绑定到代码中的公共数据字段,并利用命令。ProductMvvm实用程序中的两个视图是标准的MVVM。下面显示了ProductSelectionView
的XAML以作说明
<UserControl x:Class="ProductMvvm.Views.ProductSelectionView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vw="clr-namespace:ProductMvvm.Views"
xmlns:vm="clr-namespace:ProductMvvm.ViewModels"
xmlns:foundation="clr-namespace:MvvmFoundation.Wpf">
<UserControl.DataContext>
<vm:ProductSelectionModel />
</UserControl.DataContext>
<Grid>
<ListBox Margin="10" Background="LightSkyBlue"
ItemsSource="{Binding DataItems}"
DisplayMemberPath="ModelName"
SelectedItem="{Binding SelectedProduct}"
foundation:CommandBehavior.RoutedEventName="SelectionChanged"
foundation:CommandBehavior.TheCommandToRun="{Binding Path=ListBoxCommand}">
</ListBox>
</Grid>
</UserControl>
两个视图都是UserControls。注意为UserControl
设置DataContext
的vm
命名空间及其定义和用法。关联的ViewModel正在由XAML实例化。XAML实例化的ViewModel需要无参数构造函数。存在预期的数据绑定。SelectedProduct
的命令以及foundation
命名空间似乎有些奇怪。稍后会对此进行更多说明。ProductDisplayView
的XAML非常相似。由于篇幅较长,此处不予显示。但是,它采用了相同的原理。
关于Foundation
MVVM应用程序会遇到一组常见问题,尤其是在存在多个视图及其关联ViewModel时。幸运的是,有各种各样的MVVM Foundation或Frameworks,由有才华的人编写,它们可以优雅地解决MVVM问题。Foundation的范围从简单到全包式的复杂解决方案,它们“无所不能”(a.k.a. Debbie)。我的方法是通过有选择地从网上可用的简单、免费的解决方案中提取我所需的部分来构建一个Foundation。Foundation目录中的任何代码都不是我编写的。Josh Smith或Sacha Barber是作者。我可能在Foundation上做得有点过头了。可以手工制作替代解决方案。但是使用Foundation产生了简单标准化的代码。我还可以通过XAML完成更多工作。解决的问题包括...
- Messenger.cs - 以发布/订阅的方式支持ViewModel之间的解耦消息传递。消息发送者不知道是否有ViewModel在接收消息。
- RelayCommand.cs - 使用委托将命令的功能中继到另一个对象。也支持
CanExecute
。 - CommandBehavior.cs - 用于将命令附加到任何WPF元素。
ViewModel通信
ProductSelectionModel
维护数据库中产品的一个可视图集合。该模型是自填充的。它访问数据库以检索所有产品。ProductDisplayModel
负责其余的数据库操作。它在添加、删除或更新产品时访问数据库。这两个ViewModel在操作员进行更改时进行通信。例如,当操作员在ProductSelection
视图中取消选择时,会通知ProductDisplayModel
取消选择,以便它知道在选择另一个产品之前无法执行删除或更新。同样,当在ProductDisplayModel
中删除产品时,会通知ProductSelectionModel
,以便它可以从其可视图集合中删除已删除的产品。ViewModel之间的通信通过Messenger
对象完成。尽管这看起来像复杂的行为,但我们可以通过查看ViewModel的构造函数来快速概览ViewModel感兴趣的消息。构造函数注册它想要接收的消息类型以及收到消息后要执行的操作。
public ProductDisplayModel()
{
Messenger messenger = App.Messenger;
messenger.Register("ProductSelectionChanged",
(Action<Product>)(param => ProcessProduct(param)));
messenger.Register("SetStatus", (Action<String>)(param => stat.Status = param));
} //ctor
public ProductSelectionModel()
{
dataItems = new MyObservableCollection();
DataItems = App.StoreDB.GetProducts(); //populate yourself
listBoxCommand = new RelayCommand(() => SelectionHasChanged());
App.Messenger.Register("ProductCleared", (Action)(() => SelectedProduct=null));
App.Messenger.Register("GetProducts",
(Action)(() => DataItems = App.StoreDB.GetProducts()));
App.Messenger.Register("UpdateProduct",
(Action<Product>)(param => UpdateProduct(param)));
App.Messenger.Register("DeleteProduct", (Action)(() => DeleteProduct()));
App.Messenger.Register("AddProduct", (Action<Product>)(param => AddProduct(param)));
}
建模产品
几个类用于表示从数据库中检索到的产品。检索到的产品包含来自Product和Category表的信息。信息是字符串和数字数据的混合。XAML视图绑定到ViewModels文件夹中Product.cs文件中定义的Product
类中的字段。出错时,XAML绑定会静默失败。例如,当操作员在绑定到数字数据字段的XAML元素中输入字符串或非法数字数据时,绑定会失败而不会显示错误。这可能会在数据字段中留下意外的值。程序通常会在远离绑定处遇到这种错误,此时某些操作会神秘地失败。
为避免XAML绑定失败,Product
类中的所有绑定字段都定义为字符串。字符串字段接受操作员输入的任何内容。任何可能的XAML绑定错误都被消除。数据字段将始终包含操作员输入的内容。使用字符串字段可以绕过任何绑定问题,但会引入另一个问题。字符串字段不能用于接受或提供数字数据库数据。Product
类非常适合将数据绑定到视图,但对于SQL操作则不可接受。SqlProduct
类用于满足SQL的要求。此类具有数字字段以及与Product
对象进行转换的方法。SqlProduct
类由负责访问数据库的单个模块使用。
另外两个类LinqProduct
和LinqCategory
由Visual Studio在映射数据库时创建。这些类在LINQ查询中使用。它们的使用是暂时的,因为它们会立即转换为Product
对象。由于很难在代码中找到这些类,因此提供了下面来自GetProducts()
的代码片段来演示其用法。
MyObservableCollection<Product> products = new MyObservableCollection<Product>();
try
{
LinqDataContext dc = new LinqDataContext();
var query = from q in dc.LinqProducts
select new SqlProduct{ //convert to SqlProduct objects
ProductId = q.ProductID, ModelNumber = q.ModelNumber,
ModelName=q.ModelName, UnitCost = (decimal)q.UnitCost,
Description = q.Description, CategoryName = q.LinqCategory.CategoryName
};
foreach (SqlProduct sp in query) //convert SqlProduct to Product
products.Add(sp.SqlProduct2Product());
} //try
使用多个类来表示产品可能看起来出乎意料或过于复杂。然而,可以使它们的用法短暂且隔离。Product
类在代码中随处可见。相比之下,SqlProduct
、LinqProduct
和LinqCategory
的使用仅限于StoreDb.cs文件,在该文件中它们用作形成Product
对象的桥梁。
错误检查产品显示
ProductDisplayViewModel
在调用SQL访问数据库之前检查数据字段。特定数据库操作所需的任何字段错误都将以红色突出显示,并在状态字段中显示错误消息。下图说明了错误显示。操作员正尝试使用非法的单位成本和缺失的类别字段来更新产品。操作员必须修复突出显示的错误字段并重新提交更新。
我选择不使用WPF的错误检查功能,因为它与应用程序的工作方式不匹配。ProductProductDisplayModelStatus
类负责错误检查,并且是产品显示ViewModel的一部分。此类根据调用哪个操作来检查特定的产品字段。自制错误检查增加了XAML和ViewModel的复杂性。但是,它允许您进行任何类型的错误检查。
ProductDisplay
视图在用于显示Product
值的每个TextBox
周围定义了BorderBrush
。操作完成后,ProductDisplayModelStatus
会检查Product
值并确定应使用哪个画笔来勾勒TextBox
。这意味着ProductDisplay
必须为每个TextBox
的BorderBrush
值提供可绑定的数据字段。下面显示了仅针对UnitCost字段的ProductDisplay
视图和ProductDisplayModelStatus
类的片段。
<TextBox Margin="5" Grid.Row="2" Grid.Column="1"
BorderBrush="{Binding Path=Stat.UnitCostBrush}" BorderThickness="1"
Text="{Binding Path=DisplayedProduct.UnitCost, UpdateSourceTrigger=PropertyChanged}">
</TextBox>
public class ProductDisplayModelStatus : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
PropertyChanged(this, e);
}
public SolidColorBrush UnitCostBrush
{
get { return unitCostBrush; }
set { unitCostBrush = value;
OnPropertyChanged(new PropertyChangedEventArgs("UnitCostBrush")); }
}
模型
xStoreDb数据库包含上面显示的两个表和四个存储过程。所有对遗留数据库的访问都通过Store.cs文件调用存储过程完成。最初,这是使用SqlCommand
完成的。在用LINQ to SQL替换SqlCommand
逻辑之前,必须映射数据库。我让Visual Studio完成了映射。向项目中添加LINQ to Sql类项会打开O/R Designer。将两个数据库表从服务器资源管理器拖到O/R Designer的左窗格就映射了表。随后,将存储过程拖到设计器的右窗格。如果您尝试将存储过程拖到左窗格的表上,您将收到一个架构错误。这完成了LINQ to SQL的数据库映射。
决定如何使用LINQ to SQL是一个个人决定。虽然我毫不犹豫地使用LINQ检索数据库信息,但我更倾向于使用存储过程的安全来修改数据库。因此,我用上面已经显示的LINQ查询替换了GetProducts()
存储过程。其余的存储过程是通过LINQ而不是SqlCommand
调用的。使用LINQ调用存储过程非常容易,并且简化了代码,使其更具可读性。所有LINQ修改都在StoreDb.cs文件中。我将遗留的SqlCommand
代码注释掉,以显示与LINQ替换的区别。下面显示了调用DeleteProduct()
的两种机制
//LEGACY SQLCOMMAND code to invoke Stored Procedure
public bool DeleteProduct(int productId)
{
hasError = false;
SqlConnection con = new SqlConnection(conString);
SqlCommand cmd = new SqlCommand("DeleteProduct", con);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add("@ProductId", SqlDbType.Int, 4);
cmd.Parameters["@ProductId"].Value = productId;
try
{
con.Open();
int rows = cmd.ExecuteNonQuery();
}
catch (Exception ex)
{
errorMessage = "DELETE error, " + ex.Message;
hasError = true;
}
finally
{
con.Close();
}
return !hasError;
}// DeleteProduct()
//LINQ TO SQL replacement code
public bool DeleteProduct(int productId)
{
hasError = false;
try
{
LinqDataContext dc = new LinqDataContext();
dc.DeleteProduct(productId);
}
catch (Exception ex)
{
errorMessage = "Delete error, " + ex.Message;
hasError = true;
}
return !hasError;
}// DeleteProduct()
凝胶按钮
凝胶按钮的外观在GelButtonResourceDictionary.xaml文件中定义。它将按钮元素的样式嵌入ResourceDictionary
中。这简化了样式在其他项目中的重用。只需将文件放入项目中并合并即可。在此项目中,将凝胶资源字典合并到App.xaml文件中的应用程序资源中。这定义了应用程序中每个按钮的外观。合并资源字典文件只需几行XAML。下面显示了App.xaml的重现
<Application x:Class="ProductMvvm.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="Window1.xaml">
<Application.Resources">
<ResourceDictionary">
<ResourceDictionary.MergedDictionaries">
<ResourceDictionary Source="GelButtonResourceDictionary.xaml"/">
</ResourceDictionary.MergedDictionaries">
</ResourceDictionary">
</Application.Resources>
</Application>
无缝图块
无缝图块代表一个随机图案,可以无限重复而不会重复。例如,拉丝金属、锈迹、大理石或木纹。该图块是在Photoshop中创建的。网络上有很多教程展示了制作重复图块的技术。基本上,您会得到一个随机图案图块。该图块不可重复,因为边缘不会对齐。然后将图块翻转,使其可以重复。现在所有边缘都混合在一起,但图块内部有可见的接缝。通过使用Photoshop修复任何内部接缝来完成图块。到目前为止,我还没有在其他WPF程序中看到过这种效果。
结束
我使用这个项目作为示例。它是创建更好事物的起点。我试图将我在网上看到的东西整合到一个包中。把它放在一起,我惊讶于我不得不去多少个不同的网站才能让MVVM工作。希望它能帮助到一些人。如果您发现任何错误或知道改进代码的方法,请告诉我。
历史
- 2010年11月02日:初始发布。