C# 面向对象编程入门指南(五)
十五、开发办公用品订购应用
在前面的章节中,你看到了三种开发应用图形用户界面的方法。图形用户界面为人类用户提供了一种与您的应用进行交互并使用它们所提供的服务的方式。您还看到了服务如何创建编程接口,其他程序可以调用这些接口来使用应用的服务,而无需任何用户交互。
在这一章中,你兜了一圈,回到了你在第四章中设计的办公用品订购应用(简称 OSO)。这一章是一个大活动,也是一次期末考试。您将创建一个功能性的应用,其中包含了您在前面章节中学到的概念。在创建应用的过程中,您应该能够识别这些概念,并将它们与前面介绍的概念联系起来。该应用将包含数据访问层、业务逻辑层和用户界面层。
读完这一章,你会明白为什么应用被分成不同的层,以及如何构造它们。
重温应用设计
设计应用时,通常可以分三个不同的阶段进行。首先,你完成概念设计,然后是逻辑设计,最后是物理设计。
如第四章所述,概念设计 构成了该过程的发现阶段。概念设计阶段涉及系统用户和系统设计者之间大量的合作和交流。系统设计者必须完全理解被提议的系统将包含的业务过程。使用场景和用例,设计者定义系统的功能需求。此阶段的必要成果是系统的开发人员和用户对系统功能和范围达成共识。
设计过程的第二阶段是逻辑设计。在逻辑设计阶段,你要设计出系统结构和组织的细节。这个阶段包括组成系统的业务对象和类的开发和识别。UML 类图标识了系统对象,您可以为这些对象标识和记录属性和行为。您还可以使用类图开发和记录这些对象的结构依赖性。使用序列图和协作图,您可以发现和识别各种系统对象之间的交互和行为依赖。这个阶段的结果,即应用对象模型,独立于任何特定于实现的技术和部署架构。
设计过程的第三阶段是物理设计。在物理设计阶段,您将应用对象模型转换成实际的系统。您评估并决定特定的技术和基础结构,进行成本分析,并确定任何限制。诸如程序员经验和知识基础、当前的实现技术和遗留系统集成等问题都会影响您在物理设计阶段的决策。您还必须分析安全问题、网络基础设施和可伸缩性要求。
设计分布式应用时,通常将其逻辑架构结构与物理架构结构分开。通过以这种方式分离架构结构,您会发现维护和更新应用要容易得多。您可以在影响最小的情况下进行任何物理架构更改(例如,提高可伸缩性)。逻辑架构设计通常将应用的各个部分分成不同的层。用户与表示层交互,表示层向用户呈现数据,并为用户提供发起业务服务请求的方法。业务逻辑层封装并实现了应用的业务逻辑。它负责执行计算、处理数据、控制应用逻辑和序列。数据层负责管理对信息的访问和存储,这些信息必须在不同的用户和业务流程之间持久化和共享。图 15-1 显示了一个典型的三层应用的不同逻辑层。
图 15-1 。三层应用的逻辑层
当您创建应用的物理层时,理想情况下,每个逻辑层都对应于其专用服务器上的一个不同的物理层。实际上,应用的物理层受到可用硬件和网络基础设施等因素的影响。您可能将所有逻辑层都放在一台物理服务器上,或者分布在 web 和数据库服务器上。重要的是,您创建的应用在类之间实现了明确的职责分离。
图 15-2 显示了 OSO 应用的建议布局。为了简单起见,您将在同一个程序集中创建业务逻辑类和数据访问类。该组件被称为业务逻辑层(BLL),而用户界面层包含在它自己的组件用户界面层(UI)中。两个程序集都包含在同一台服务器(您的计算机)上。需要记住的重要一点是,由于业务逻辑类和数据访问类之间有明确的职责分离,随着应用功能和用户的增加,它可以很容易地重构为托管在不同服务器上的不同程序集。
图 15-2 。OSO 应用的物理层
构建 OSO 应用的数据访问层
为了开发应用的业务逻辑和数据访问层,你需要回顾你在第四章中创建的 OSO 类图(如图 15-3 所示)。
图 15-3 。OSO 应用类图
正如第四章所讨论的,你需要创建一个Employee类来实现一个登录方法(Login())。登录方法将与数据库交互,以验证登录信息。为此,您将创建两个雇员类:一个用于业务逻辑层(Employee),一个用于数据访问层(DALEmployee)。Employee类将把来自用户界面(UI)的登录请求传递给DALEmployee类,后者将与数据库交互以检索所请求的信息。图 15-4 是办公用品数据库的数据库模式。此数据库驻留在 SQL Server 数据库中。
图 15-4 。办公用品数据库图表
注如果您没有安装办公用品数据库,请参见附录 C 获取说明。
首先,您将从数据访问层开始,然后实现业务逻辑层。
在 Visual Studio 中,创建一个类库应用,命名为 OfficeSupplyBLL,将解决方案命名为 OfficeSupply,如图图 15-5;该应用将包含 OSO 应用的业务逻辑层和数据访问层的类。
图 15-5 。创建 OfficeSupplyBLL 类库
注意如果你不想从头开始编写办公用品订购应用,你可以从 Apress 网站下载。详见附录 C 。
接下来,您将创建一个静态类(DALUtility),它实现从 App.config 文件中检索数据库连接字符串。其他类将调用它的GetSQLConnection来检索连接字符串。
将 App.config 文件添加到项目中。在 App.config 文件中,添加下面的 xml 代码来设置连接信息。请记住,根据您的服务器设置,您可能需要更改数据源。
<configuration>
<connectionStrings>
<add name="OSConnection"
providerName="System.Data.SqlClient"
connectionString="data source=localhost;initial catalog=OfficeSupply;
integrated security=True;"/>
</connectionStrings>
</configuration>
在“解决方案资源管理器”窗口中,右击“引用”文件夹并选择“添加引用”。在框架组件中,选择System.Configuration组件,如图图 15-6 所示。
图 15-6 。添加对应用的引用
向项目中添加一个类,并将其命名为DALUtility。将以下代码添加到类文件中:
using System;
using System.Configuration;
namespace OfficeSupplyBLL
{
public static class DALUtility
{
public static string GetSQLConnection(string name)
{
// Assume failure.
string returnValue = null;
// Look for the name in the connectionStrings section.
ConnectionStringSettings settings
= ConfigurationManager.ConnectionStrings[name];
// If found, return the connection string.
if (settings != null)
{
returnValue = settings.ConnectionString;
}
return returnValue;
}
}
}
下一个要添加的类是DALEmployee类。这个类包含一个Login方法,用于检查提供给数据库中的值的用户名和密码。它使用一个SQLCommand对象对数据库执行一条 SQL 语句。如果找到匹配,它返回雇员的userId.,如果没有找到匹配,它返回-1。因为 SQL 语句返回一个值,所以可以使用SQLCommand对象的ExecuteScalar方法。
将名为DALEmployee的类添加到应用中,并将以下代码插入到类文件中:
using System;
using System.Data.SqlClient;
namespace OfficeSupplyBLL
{
class DALEmployee
{
public int LogIn(string userName, string password)
{
string connString = DALUtility.GetSQLConnection("OSConnection");
using( SqlConnection conn = new SqlConnection(connString))
{
using (SqlCommand cmd = new SqlCommand())
{
cmd.Connection = conn;
cmd.CommandText = "Select EmployeeID from Employee where "
+ " UserName = @UserName and Password = @Password ";
cmd.Parameters.AddWithValue("@UserName", userName);
cmd.Parameters.AddWithValue("@Password", password);
int userId;
conn.Open();
userId = (int)cmd.ExecuteScalar();
if (userId > 0)
{
return userId;
}
else
{
return -1;
}
}
}
}
}
}
下一个要构建的类是DALProductCatalog类,其目的是封装应用检索和列出数据库中可用产品所需的功能。您还希望能够根据产品所属的类别来查看产品。您需要的信息在两个数据库表中:目录表和产品表。这两个表通过CatID字段相关联。
当客户端请求产品目录信息时,会创建一个数据集并将其返回给客户端。该服务在DALProductCatalog类的GetProductInfo方法中提供。将名为DALProductCatalog的类添加到项目中,并插入如下所示的代码:
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OfficeSupplyBLL
{
public class DALProductCatalog
{
public DataSet GetProductInfo()
{
DataSet _dsProducts;
string connString = DALUtility.GetSQLConnection("OSConnection");
using (SqlConnection _conn = new SqlConnection(connString))
{
_dsProducts = new DataSet("Products");
//Get category info
String strSQL = "Select CategoryId, Name, Description from Category";
using(SqlCommand cmdSelCategory = new SqlCommand(strSQL, _conn))
{
using(SqlDataAdapter daCatagory = new SqlDataAdapter(cmdSelCategory))
{
daCatagory.Fill(_dsProducts, "Category");
}
}
//Get product info
String strSQL2 = "Select ProductID, CategoryID, Name," +
"Description, UnitCost from Product";
using(SqlCommand cmdSelProduct = new SqlCommand(strSQL2, _conn))
{
using (SqlDataAdapter daProduct = new SqlDataAdapter(cmdSelProduct))
{
daProduct.Fill(_dsProducts, "Product");
}
}
}
//Set up the table relation
DataRelation drCat_Prod = new DataRelation("drCat_Prod",
_dsProducts.Tables["Category"].Columns["CategoryID"],
_dsProducts.Tables["Product"].Columns["CategoryID"], false);
_dsProducts.Relations.Add(drCat_Prod);
return _dsProducts;
}
}
}
当客户端准备提交订单时,它将调用Order类的PlaceOrder方法,稍后您将在业务逻辑类中定义这个类。客户机将雇员 ID 传递给方法,并接收一个订单编号作为返回值。Order类的PlaceOrder方法将以 XML 字符串的形式将订单信息传递给DALOrder类进行处理。DALOrder类包含PlaceOrder方法,该方法从Order类接收 XML 订单字符串,并将其传递给 SQL Server 数据库中的存储过程。存储过程更新数据库并传回订单号。然后,这个订单号被返回给Order类,该类又将订单号传递回客户端。
将名为DALOrder的类添加到项目中,并将以下代码插入到类文件中:
using System;
using System.Data;
using System.Data.SqlClient;
namespace OfficeSupplyBLL
{
class DALOrder
{
public int PlaceOrder(string xmlOrder)
{
string connString = DALUtility.GetSQLConnection("OSConnection");
using (SqlConnection cn = new SqlConnection(connString))
{
using (SqlCommand cmd = cn.CreateCommand())
{
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandText = "up_PlaceOrder";
SqlParameter inParameter = new SqlParameter();
inParameter.ParameterName = "@xmlOrder";
inParameter.Value = xmlOrder;
inParameter.DbType = DbType.String;
inParameter.Direction = ParameterDirection.Input;
cmd.Parameters.Add(inParameter);
SqlParameter ReturnParameter = new SqlParameter();
ReturnParameter.ParameterName = "@OrderID";
ReturnParameter.Direction = ParameterDirection.ReturnValue;
cmd.Parameters.Add(ReturnParameter);
int intOrderNo;
cn.Open();
cmd.ExecuteNonQuery();
cn.Close();
intOrderNo = (int)cmd.Parameters["@OrderID"].Value;
return intOrderNo;
}
}
}
}
}
现在您已经构建了数据访问层类,您已经准备好构建业务逻辑层类集了。
构建 OSO 应用的业务逻辑层
将名为Employee的类添加到项目中。这个类将封装 UI 使用的雇员信息,并将登录请求传递给数据访问层。将以下代码添加到 Employee.cs 文件中:
using System;
namespace OfficeSupplyBLL
{
public class Employee
{
int _employeeID;
public int EmployeeID
{
get { return _employeeID; }
set { _employeeID = value; }
}
string _loginName;
public string LoginName
{
get { return _loginName; }
set { _loginName = value; }
}
string _password;
public string Password
{
get { return _password; }
set { _password = value; }
}
Boolean _loggedIn = false;
public Boolean LoggedIn
{
get { return _loggedIn; }
}
public Boolean LogIn()
{
DALEmployee dbEmp = new DALEmployee();
int empId;
empId = dbEmp.LogIn(this.LoginName, this.Password);
if (empId > 0)
{
this.EmployeeID = empId;
this._loggedIn = true;
return true;
}
else
{
this._loggedIn = false;
return false;
}
}
}
}
ProductCatalog类向 UI 提供产品数据集。它从DALProductCatalog类中检索数据集。在将数据集传递给 UI 之前,您可以在数据集上执行任何业务逻辑。向项目中添加一个新的ProductCatalog类文件,并添加以下代码来实现ProductCatalog类。
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OfficeSupplyBLL
{
public class ProductCatalog
{
public DataSet GetProductInfo()
{
//perform any business logic befor passing to client.
// None needed at this time.
DALProductCatalog prodCatalog = new DALProductCatalog();
return prodCatalog.GetProductInfo();
}
}
}
当用户向订单添加项目时,订单项目信息被封装在一个OrderItem 类中。这个类实现了INotifyPropertyChanged接口。此接口是通知 UI 某个属性已更改所必需的,以便它可以更新绑定到该类的任何控件。它还覆盖了ToString方法,以提供包含商品信息的 XML 字符串。当下订单时,这个字符串将被传递给 DAL。
将OrderItem类添加到项目中,并插入以下代码来实现它。
using System;
using System.ComponentModel;
namespace OfficeSupplyBLL
{
public class OrderItem : INotifyPropertyChanged
{
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected void Notify(string propName)
{
if (this.PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
#endregion
string _ProdID;
int _Quantity;
double _UnitPrice;
double _SubTotal;
public string ProdID
{
get { return _ProdID; }
set { _ProdID = value; }
}
public int Quantity
{
get { return _Quantity; }
set
{
_Quantity = value;
Notify("Quantity");
}
}
public double UnitPrice
{
get { return _UnitPrice; }
set { _UnitPrice = value; }
}
public double SubTotal
{
get { return _SubTotal; }
}
public OrderItem(String productID, double unitPrice, int quantity)
{
_ProdID = productID;
_UnitPrice = unitPrice;
_Quantity = quantity;
_SubTotal = _UnitPrice * _Quantity;
}
public override string ToString()
{
string xml = "<OrderItem";
xml += " ProductID='" + _ProdID + "'";
xml += " Quantity='" + _Quantity + "'";
xml += " />";
return xml;
}
}
}
业务逻辑层的最后一个类是Order类。该类负责维护订单项的集合。它有添加和删除项目的方法,以及在下订单时将项目传递给DALOrder类的方法。
在项目中添加了一个Order类之后,使用下面的代码来实现这个类:
using System;
using System.Collections.ObjectModel;
namespace OfficeSupplyBLL
{
public class Order
{
ObservableCollection<OrderItem> _orderItemList = new
ObservableCollection<OrderItem>();
public ObservableCollection<OrderItem> OrderItemList
{
get { return _orderItemList; }
}
public void AddItem(OrderItem orderItem)
{
foreach (var item in _orderItemList)
{
if (item.ProdID == orderItem.ProdID)
{
item.Quantity += orderItem.Quantity;
return;
}
}
_orderItemList.Add(orderItem);
}
public void RemoveItem(string productID)
{
foreach (var item in _orderItemList)
{
if (item.ProdID == productID)
{
_orderItemList.Remove(item);
return;
}
}
}
public double GetOrderTotal()
{
if (_orderItemList.Count == 0)
{
return 0.00;
}
else
{
double total = 0;
foreach (var item in _orderItemList)
{
total += item.SubTotal;
}
return total;
}
}
public int PlaceOrder(int employeeID)
{
string xmlOrder;
xmlOrder = "<Order EmployeeID='" + employeeID.ToString() + "'>";
foreach (var item in _orderItemList)
{
xmlOrder += item.ToString();
}
xmlOrder += "</Order>";
DALOrder dbOrder = new DALOrder();
return dbOrder.PlaceOrder(xmlOrder);
}
}
}
现在,您已经构建了 OSO 应用的数据访问和业务逻辑层,您已经准备好构建用户界面(UI)了。因为您在不同的层中创建了应用,所以您可以选择使用基于 Windows 的 WPF 用户界面、基于 ASP.NET 网络的用户界面或 Windows 应用商店应用。您还可以根据用户用来与应用交互的设备来创建多个 UI。在下一节中,您将构建一个 WPF 应用,用户将使用它从他们的桌面计算机上订购办公用品。
创建 OSO 应用用户界面
为了创建订购系统的 WPF 接口,您需要向包含 OfficeSupplyBLL 项目的解决方案中添加一个 WPF 项目。
在 Visual Studio 中,向解决方案中添加一个 WPF 项目,并将其命名为 OfficeSupplyWPF。项目加载后,添加对 OfficeSupplyBLL 项目的引用,如图图 15-7 所示。
图 15-7 。添加对 OfficeSupplyBLL 项目的引用
在 App.config 文件中,将下面的 xml 代码添加到配置结束标记之前。请记住,根据您的服务器设置,您可能需要更改数据源。
<connectionStrings>
<add name="OSConnection"
providerName="System.Data.SqlClient"
connectionString="data source=localhost;initial catalog=OfficeSupply;
integrated security=True;"/>
</connectionStrings>
用户界面的第一个目标是展示可以订购的产品信息。产品信息呈现在数据网格控件中。用户将通过选择 ComboBox 控件中的类别来查看特定类别中的产品。产品列出后,用户可以将产品添加到订单中。当一个产品被添加到订单中时,它会显示在数据网格下面的列表视图中。图 15-8 显示了在订单中添加了项目的 OSO 订单。
图 15-8 。向订单添加项目的表单
将以下 XAML 代码添加到 MainWindow.xaml 文件中,以创建 OSO 订单。请注意数据绑定在各种控件中的使用。例如,数据网格的项目来源被设置为类别表和产品表之间的数据关系(ItemsSource="{Binding drCat_Prod}")。这允许数据网格显示下拉列表中所选类别的产品。
<Window x:Class="OfficeSupplyWPF.MainWindow"
FontName1">http://schemas.microsoft.com/winfx/2006/xaml/presentation "
xmlns:x=" http://schemas.microsoft.com/winfx/2006/xaml "
Title="Office Supply Ordering" Height="484" Width="550" Loaded="Window_Loaded">
<Grid>
<StackPanel Name="LayoutRoot" DataContext="{Binding}"
Orientation="Vertical" HorizontalAlignment="Left" Height="auto" Width="auto">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<Label Content="Categories:" Margin="10,0,0,0"/>
<ComboBox ItemsSource="{Binding}" Name="categoriesComboBox"
IsSynchronizedWithCurrentItem="True"
DisplayMemberPath="Name" Height="23" Margin="12" Width="200" >
<ComboBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ComboBox.ItemsPanel>
</ComboBox>
<Button Content="Login" Height="30" Name="loginButton"
Width="75" Margin="20,5,0,0" Click="loginButton_Click" />
<Button Content="Exit" Height="30" Name="exitButton"
Width="75" Margin="20,5,0,0" Click="exitButton_Click" />
</StackPanel>
<DataGrid AutoGenerateColumns="False" Height="165"
ItemsSource="{Binding drCat_Prod}"
Name="ProductsDataGrid" RowDetailsVisibilityMode="VisibleWhenSelected"
Width="490" HorizontalAlignment="Left" Margin="20,0,20,10"
SelectionMode="Single">
<DataGrid.Columns>
<DataGridTextColumn
x:Name="productIDColumn" Binding="{Binding Path=ProductID}"
Header="Product ID" Width="40*" />
<DataGridTextColumn
x:Name="nameColumn" Binding="{Binding Path=Name}"
Header="Name" Width="40*" />
<DataGridTextColumn
x:Name="descriptColumn" Binding="{Binding Path=Description}"
Header="Description" Width="80*" />
<DataGridTextColumn
x:Name="unitCostColumn" Binding="{Binding Path=UnitCost}"
Header="Unit Cost" Width="30*" />
</DataGrid.Columns>
</DataGrid>
<StackPanel Orientation="Vertical">
<ListView Name="orderListView" MinHeight="150" Width="490"
ItemsSource="{Binding}" SelectionMode="Single">
<ListView.View>
<GridView>
<GridViewColumn Width="140" Header="Product Id"
DisplayMemberBinding="{Binding ProdID}" />
<GridViewColumn Width="140" Header="Unit Price"
DisplayMemberBinding="{Binding UnitPrice}" />
<GridViewColumn Width="140" Header="Quantity"
DisplayMemberBinding="{Binding Quantity}" />
</GridView>
</ListView.View>
</ListView>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Name="addButton" MinHeight="25" MinWidth="80"
Content="Add Item" Click="addButton_Click" />
<Button Name="removeButton" MinHeight="25" MinWidth="80"
Content="Remove Item" Click="removeButton_Click"/>
<Button Name="placeOrderButton" MinHeight="25" MinWidth="80"
Content="Place Order" Click="placeOrderButton_Click"/>
</StackPanel>
</StackPanel>
<StatusBar VerticalAlignment="Bottom" HorizontalAlignment="Stretch">
<TextBlock Name="statusTextBlock">You must log in to place an order.</TextBlock>
</StatusBar>
</Grid>
</Window>
要添加订单项,用户首先在数据网格中选择一行,然后选择“添加项”按钮。“添加项目”按钮显示一个对话框,用户可以使用该对话框输入数量并添加项目。图 15-9 显示了订单项目对话框。
图 15-9 。订单项目对话框
向名为 OrderItemDialog.xaml 的项目添加一个新窗口。添加以下 xaml 代码以创建OrderItemDialog表单:
<Window x:Class="OfficeSupplyWPF.OrderItemDialog"
FontName1">http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
WindowStartupLocation="CenterOwner"
Title="Order Item" Height="169" Width="300">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<Label Grid.Column="0" Grid.Row="0" Margin="2">Product Id:</Label>
<TextBox Name="productIdTextBox" Grid.Column="1"
Grid.Row="0" Margin="2" Grid.ColumnSpan="2" IsEnabled="False"/>
<Label Grid.Column="0" Grid.Row="1" Margin="2">Unit Price:</Label>
<TextBox Name="unitPriceTextBox" Grid.Column="1"
Grid.Row="1" Margin="2" Grid.ColumnSpan="2" IsEnabled="False"/>
<Label Grid.Column="0" Grid.Row="2" Margin="2" >Quantity:</Label>
<TextBox Name="quantityTextBox" Grid.Column="1"
Grid.Row="2" Margin="2" MinWidth="80" Text="1"/>
<StackPanel Grid.Column="0" Grid.ColumnSpan="3"
Grid.Row="3" Orientation="Horizontal"
HorizontalAlignment="Center">
<Button Name="okButton" Click="okButton_Click" IsDefault="True"
MinWidth="80" Margin="5">OK</Button>
<Button Name="cancelButton" Click="cancelButton_Click" IsCancel="True"
MinWidth="80" Margin="5">Cancel</Button>
</StackPanel>
</Grid>
</Window>
用户必须先登录,然后才能提交订单。当他们点击登录按钮时,会出现一个登录对话窗口,如图图 15-10 所示。
图 15-10 。登录对话框
向名为 LoginDialog.xaml 的项目添加一个新窗口。
<Window x:Class="OfficeSupplyWPF.LoginDialog"
FontName1">http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Login" Height="131" Width="300"
WindowStartupLocation="CenterOwner"
FocusManager.FocusedElement="{Binding ElementName=nameTextBox}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<Label Grid.Column="0" Grid.Row="0" Margin="2">Name:</Label>
<TextBox Name="nameTextBox" Grid.Column="1" Grid.Row="0" Margin="2"/>
<Label Grid.Column="0" Grid.Row="1" Margin="2">Password:</Label>
<PasswordBox Name="passwordTextBox" Grid.Column="1" Grid.Row="1" Margin="2"/>
<StackPanel Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="2"
Orientation="Horizontal" HorizontalAlignment="Center">
<Button Name="okButton" Click="okButton_Click" IsDefault="True"
MinWidth="80" Margin="5">OK</Button>
<Button Name="cancelButton" Click="cancelButton_Click" IsCancel="True"
MinWidth="80" Margin="5">Cancel</Button>
</StackPanel>
</Grid>
</Window>
现在,您已经创建了构成 UI 的窗口,您可以将实现添加到窗口的代码隐藏文件中了。
将以下代码添加到 MainWindow.xaml.cs 代码隐藏文件中。下面是对代码的讨论。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Data;
using OSOBLL;
using System.Collections.ObjectModel;
namespace OfficeSupplyWPF
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
DataSet _dsProdCat;
Employee _employee;
Order _order;
public MainWindow()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
ProductCatalog prodCat = new ProductCatalog();
_dsProdCat = prodCat.GetProductInfo();
this.DataContext = _dsProdCat.Tables["Category"];
_order = new Order();
_employee = new Employee();
this.orderListView.ItemsSource = _order.OrderItemList;
}
private void loginButton_Click(object sender, RoutedEventArgs e)
{
LoginDialog dlg = new LoginDialog();
dlg.Owner = this;
dlg.ShowDialog();
// Process data entered by user if dialog box is accepted
if (dlg.DialogResult == true)
{
_employee.LoginName = dlg.nameTextBox.Text;
_employee.Password = dlg.passwordTextBox.Password;
if (_employee.LogIn() == true)
{
this.statusTextBlock.Text = "You are logged in as employee number " +
_employee.EmployeeID.ToString();
}
else
{
MessageBox.Show("You could not be verified. Please try again.");
}
}
}
private void exitButton_Click(object sender, RoutedEventArgs e)
{
this.Close();
}
private void addButton_Click(object sender, RoutedEventArgs e)
{
OrderItemDialog orderItemDialog = new OrderItemDialog();
DataRowView selectedRow;
selectedRow = (DataRowView)this.ProductsDataGrid.SelectedItems[0];
orderItemDialog.productIdTextBox.Text = selectedRow.Row.ItemArray[0].ToString();
orderItemDialog.unitPriceTextBox.Text = selectedRow.Row.ItemArray[4].ToString();
orderItemDialog.Owner = this;
orderItemDialog.ShowDialog();
if (orderItemDialog.DialogResult == true )
{
string productId = orderItemDialog.productIdTextBox.Text;
double unitPrice = double.Parse(orderItemDialog.unitPriceTextBox.Text);
int quantity = int.Parse(orderItemDialog.quantityTextBox.Text);
_order.AddItem(new OrderItem(productId,unitPrice,quantity));
}
}
private void removeButton_Click(object sender, RoutedEventArgs e)
{
if (this.orderListView.SelectedItem != null)
{
var selectedOrderItem = this.orderListView.SelectedItem as OrderItem;
_order.RemoveItem(selectedOrderItem.ProdID);
}
}
private void placeOrderButton_Click(object sender, RoutedEventArgs e)
{
if (_employee.LoggedIn == true)
{
//place order
int orderId;
orderId = _order.PlaceOrder(_employee.EmployeeID);
MessageBox.Show("Your order has been placed. Your order id is " +
orderId.ToString());
}
else
{
MessageBox.Show("You must be logged in to place an order.");
}
}
}
}
看一下前面的代码就会发现,当窗口加载时,Window_Loaded事件检索ProdCat数据集,并将其设置为等于窗口的DataContext,以便 ComboBox 和 GridView 控件可以绑定到它。一个Order对象被创建,ListView 控件被绑定到它的OrderItem集合。
private void Window_Loaded(object sender, RoutedEventArgs e)
{
ProductCatalog prodCat = new ProductCatalog();
_dsProdCat = prodCat.GetProductInfo();
this.DataContext = _dsProdCat.Tables["Category"];
_order = new Order();
_employee = new Employee();
this.orderListView.ItemsSource = _order.OrderItemList;
}
loginButton_Click 事件启动 LoginDialog 窗口的一个实例,并检查对话结果。如果返回 true,_employee对象的值被设置为对话框中输入的值,并且调用Employee类的Login方法。如果Login方法返回 true,用户被通知他们已经登录。
private void loginButton_Click(object sender, RoutedEventArgs e)
{
LoginDialog dlg = new LoginDialog();
dlg.Owner = this;
dlg.ShowDialog();
// Process data entered by user if dialog box is accepted
if (dlg.DialogResult == true)
{
_employee.LoginName = dlg.nameTextBox.Text;
_employee.Password = dlg.passwordTextBox.Password;
if (_employee.LogIn() == true)
{
this.statusTextBlock.Text = "You are logged in as employee number " +
_employee.EmployeeID.ToString();
}
else
{
MessageBox.Show("You could not be verified. Please try again.");
}
}
}
addButton_Click事件启动 OrderItemDialog 窗口的一个实例,并用 ProductsDataGrid 的选定行中的信息填充文本框。如果DialogResult返回 true,则在对话框中输入的信息用于创建一个OrderItem对象,并将其添加到订单的OrderItem集合中。
private void addButton_Click(object sender, RoutedEventArgs e)
{
OrderItemDialog orderItemDialog = new OrderItemDialog();
DataRowView selectedRow;
selectedRow = (DataRowView)this.ProductsDataGrid.SelectedItems[0];
orderItemDialog.productIdTextBox.Text = selectedRow.Row.ItemArray[0].ToString();
orderItemDialog.unitPriceTextBox.Text = selectedRow.Row.ItemArray[4].ToString();
orderItemDialog.Owner = this;
orderItemDialog.ShowDialog();
if (orderItemDialog.DialogResult == true )
{
string productId = orderItemDialog.productIdTextBox.Text;
double unitPrice = double.Parse(orderItemDialog.unitPriceTextBox.Text);
int quantity = int.Parse(orderItemDialog.quantityTextBox.Text);
_order.AddItem(new OrderItem(productId,unitPrice,quantity));
}
}
removeButton_Click事件检查是否在orderListView中选择了一个项目,并将其从订单中删除。
private void removeButton_Click(object sender, RoutedEventArgs e)
{
if (this.orderListView.SelectedItem != null)
{
var selectedOrderItem = this.orderListView.SelectedItem as OrderItem;
_order.RemoveItem(selectedOrderItem.ProdID);
}
}
placeOrderButton_Click事件检查用户是否登录,如果登录就下订单。
private void placeOrderButton_Click(object sender, RoutedEventArgs e)
{
if (_employee.LoggedIn == true)
{
//place order
int orderId;
orderId = _order.PlaceOrder(_employee.EmployeeID);
MessageBox.Show("Your order has been placed. Your order id is " + orderId.ToString());
}
else
{
MessageBox.Show("You must be logged in to place an order.");
}
}
现在主窗口的代码隐藏已经实现,您可以为对话框窗口添加代码隐藏了。
将以下代码添加到 OrderItemDialog.xaml.cs 代码隐藏文件中。如果用户点击 OK 按钮,则DialogResult被设置为true。如果用户点击取消,则DialogResult被设置为false。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace OfficeSupplyWPF
{
/// <summary>
/// Interaction logic for OrderItemDialog.xaml
/// </summary>
public partial class OrderItemDialog : Window
{
public OrderItemDialog()
{
InitializeComponent();
}
private void okButton_Click(object sender, RoutedEventArgs e)
{
this.DialogResult = true;
}
private void cancelButton_Click(object sender, RoutedEventArgs e)
{
this.DialogResult = false;
}
}
}
将以下代码添加到 LoginDialog.xaml.cs 代码隐藏文件中。它类似于 OrderItemDialog 代码。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace OfficeSupplyWPF
{
/// <summary>
/// Interaction logic for LoginDialog.xaml
/// </summary>
public partial class LoginDialog : Window
{
public LoginDialog()
{
InitializeComponent();
}
private void okButton_Click(object sender, RoutedEventArgs e)
{
this.DialogResult = true;
}
private void cancelButton_Click(object sender, RoutedEventArgs e)
{
this.DialogResult = false;
}
}
}
现在,您已经将实现代码添加到 UI 中,您已经准备好测试应用了。花点时间玩玩这个应用。向订单中添加一些项目,并测试从订单中删除一些项目。尝试下订单(用户的有效登录是JSmith,密码是js)。当您使用这个应用时,想想您可以做些什么来改进它。需要改进的一个方面是对可能发生的错误进行捕获。例如,如果您单击“添加项目”按钮而没有在网格中选择项目,程序将会崩溃。处理这种情况的一种方法是禁用按钮,除非选择了网格行。
您可能还想扩展应用。例如,您可以实现 Windows 应用商店应用接口。另一个有趣的项目是开发一个接受应用下订单的 web 服务。
注意虽然这是一个功能应用,但它仅用于演示目的,还没有准备好投入生产。
摘要
在这一章中,你回顾了在第四章中设计的办公用品订购(OSO)应用。您创建了一个功能性应用,其中包含了您在前面章节中学到的概念。该应用包含数据访问层、业务逻辑层和用户界面层。您了解了为什么应用被分成不同的层,以及如何构建由不同层组成的工作应用。虽然您没有创建基于 web 的用户界面应用层,但因为您在不同的层中创建了应用,所以您可以轻松地将基于 Windows 的 WPF UI 替换为基于 web 的 UI 或 Windows 应用商店应用。您还使用 ADO.NET 创建了数据访问层。另一种选择是使用实体框架(EF)。您还可以创建一个 web 服务来将订单发送给供应商。
十六、总结
如果你已经走了这么远,花点时间拍拍自己的背。自从你第一次打开这本书的封面以来,你已经走过了漫长的道路;您已经获得了宝贵的技能和概念,您可以使用 .NET 框架、C# 和 Visual Studio IDE。这些包括但不限于以下内容:
- 应用设计周期的重要性
- 统一建模语言(UML)以及它如何帮助促进面向对象程序的分析和设计
- 公共语言运行时(CLR)
- 的结构 .NET 框架
- 如何创建和使用类结构和层次结构
- 如何实现继承、多态和接口
- 对象交互和协作
- 事件驱动编程
- 结构化错误处理
- 如何使用 ADO.NET 处理数据结构和数据源
- 使用实体框架创建到 SQL Server 数据库的对象关系映射
- 如何使用 Visual Studio IDE 的功能来提高生产率和简化调试
- 如何使用 Windows 演示框架实现基于 Windows 的图形用户界面
- 如何使用 ASP.NET 和 web 窗体实现基于 Web 的图形用户界面
- 如何创建 Windows 应用商店应用
- 如何使用 Windows Communication Foundation 和 ASP.NET Web API 创建和使用服务
恭喜你!你现在可以称自己为 C# 程序员(尽管是个新手)。但是,不要把自己看得太高。如果你的目标是成为一名专业 C# 程序员,你的旅程才刚刚开始。你发展的下一个阶段是积累经验。换句话说,设计和编码,然后再设计和编码。如果你在工作中设计和编写 C# 代码,这将会很容易。(尽管在他们送你去上了三天的课程后,如果你被期望成为一名专家,这将会很有压力!)
如果你是自学,你必须找到时间和项目来学习。这比你想象的要简单。每天花一个小时,想出一个计划的点子。例如,您可以设计一个程序,将菜谱转换成可扩展标记语言(XML)数据。然后,XML 数据可以生成购物清单。见鬼,如果你真的想全力以赴,那就加入一个库存追踪系统,追踪你库存中的配料。无论你如何获得经验,记住这条重要的格言:要么使用它,要么失去它!
以下部分强调了在开发编程技能时需要考虑的一些其他重要事项。
提高您的面向对象设计技能
面向对象的分析和设计是你作为程序员要完成的两项最困难的任务。这些技能对大多数程序员来说并不容易。然而,它们是你应该努力掌握的两项最重要的技能。他们就是我所说的程序员和程序员的区别。如果你和大多数首席信息官和编程经理交谈,找到编码员很容易;他们要的是程序员。**
**记住,没有一种“正确”的方法,而是几种同样有效的方法。
调查 .NET Framework 命名空间
那个 .NET Framework 包含大量的类、接口和其他类型,旨在优化和加快您的开发工作。组成的各种命名空间 .NET Framework 类库是按功能组织的。花时间熟悉这些名称空间提供的功能很重要。
从包含您最常使用的功能的名称空间开始,比如根名称空间System和System.IO,其中包含。用于读写流和文件的. NET Framework 类。
在熟悉了更常见的名称空间之后,探索一些更难理解的名称空间。例如,System.Security.Cryptography提供数据编码、哈希、消息认证等密码服务。您将会惊讶于该框架所提供的支持范围。您可以在 Visual Studio 的集成文档中找到有关各种命名空间成员的大量信息。
熟悉 ADO.NET 和实体框架
数据是编程的基础。你在你写的每个程序中存储、检索和操作数据。程序在执行过程中使用的数据结构是非持久数据——它保存在 ram 中。当应用终止时,这些数据会丢失,并且必须在下次应用运行时重新创建。持久数据是保存在永久数据结构中的数据,如文件系统或数据库。大多数程序需要从某种持久的数据存储中检索数据,并将数据持久存储到其中。这就是 ADO.NET 介入的地方。ADO.NET 是指包含处理持久数据的功能的名称空间。(它还包含在熟悉的关系数据库或 XML 类型的结构中组织和处理非持久数据的功能。)虽然我已经向您介绍了 ADO.NET 和实体框架,但这是一个非常重要的主题,值得专门写一本书来介绍这些数据访问技术。(别担心——有很多!)这绝对是一个你需要投入进一步研究的领域。要了解有关这些技术的更多信息,请访问数据开发人员中心网站http://msdn.microsoft.com/en-us/data。
了解有关创建优秀用户界面(UI)的更多信息
尽管我向你介绍了 ASP.NET 的 WPF 和 Windows 应用商店,但我只是触及了这些强大技术的皮毛。ASP.NET WPF 和 Windows 应用商店的应用包含了丰富的功能,可以在 web、桌面和移动设备上开发引人入胜的交互式用户体验。有关 WPF 编程的更多信息,请访问位于http://windowsclient.net的 Windows 客户端开发中心。欲了解更多关于 ASP.NET 编程的信息,请访问位于http://www.asp.net的开发者中心。如果您对 Windows Store 应用开发感兴趣,请访问位于http://msdn.microsoft.com/en-US/windows/apps/br229512的开发者中心。这些网站充满了学习材料和展示这些技术威力的演示应用。
转向基于组件的开发
在你掌握了面向对象的开发和你的编程逻辑在类系统中的封装之后,你就可以向基于组件的开发迈进了。组件是进一步封装编程逻辑功能的集合。尽管 OSO 应用的业务逻辑层与数据访问层在逻辑上是隔离的,但实际上它们位于同一个程序集中。通过将每个编译成自己的程序集,可以增加代码的维护和重用。您应该开始转向乐高式的应用开发方法。在这种情况下,您的应用由一组独立的部分(程序集)组成,这些部分可以连接在一起,协同工作以执行必要的服务。关于这个和其他最佳实践的更多信息,请访问微软的模式和实践网站http://pnp.azurewebsites.net/en-us/ .
查找帮助
上有大量的信息 .NET 框架和 C# 编程语言。Visual Studio 提供的帮助系统对于程序员来说是一个极好的资源。养成虔诚地使用这种资源的习惯。
另一个极其重要的资源是http://msdn.microsoft.com。该网站由 Microsoft 为开发人员提供,包含大量信息,包括白皮书、教程和网络广播研讨会;老实说,这是业内信息量最大的网站之一。如果您正在使用 Microsoft 技术开发您的编程技能,那么访问这个站点应该像阅读日报一样成为一种常规。也有一些独立的网站致力于各种 .NET 编程语言。一个很好的网站是 C# Corner ( http://www.c-sharpcorner.com/),它包含了大量关于 C# 编程各个方面的文章。您可以使用您最喜欢的搜索引擎来发现 Web 上其他致力于 C# 编程的好站点。
加入用户组
微软为本地的发展提供了大量的支持。网络用户群。用户组由对以下感兴趣的成员组成 .NET 编程。这些小组为学习、指导和建立关系网提供了很好的途径。有一个列表。在http://msdn.microsoft.com可用的网络用户组。国际 .NET 协会(INETA)也为。网络用户群体;您可以在http://www.ineta.org找到 INETA 附属用户组的列表。
如果你在你所在的地区找不到. NET 用户组,见鬼,为什么不创建一个呢?
请提供反馈
虽然已经尽了一切努力为您提供一个没有错误的文本,但在编辑过程中还是不可避免地会出现一些错误。我致力于在 Apress 网站(http://www.apress.com)上提供更新的勘误表,但是没有你的帮助我无法做到。如果你在阅读这篇文章时遇到任何错误,请通过新闻网站向我报告。
谢谢你,祝你好运!
我真诚地希望你发现阅读这篇文章是一次愉快而有价值的经历。我想感谢你让我成为你这次旅程的向导。正如你作为开发人员的技能会因为阅读这本书而提高一样,我作为开发人员的技能也会因为写这本书而大大提高。我在过去 20 年的教学和培训经验告诉我,只有当你能把一门课程教给别人时,你才能真正理解这门课程。所以,再次感谢你,祝你好运!**
十七、附录一:基本编程概念
以下信息是为编程新手和需要一些基本编程概念入门的读者准备的。如果您已经用另一种语言编程,那么本附录中的概念对您来说可能并不陌生。但是,您应该简要地回顾一下这些资料,以便熟悉 C# 语法。
使用变量和数据类型
编程语言中的变量存储的值可以在程序执行时改变。例如,如果您想计算用户尝试登录应用的次数,您可以使用一个变量来跟踪尝试次数。变量是存储值的内存位置。使用该变量,您的程序可以读取或更改存储在内存中的值。但是,在程序中使用变量之前,必须声明它。当你声明一个变量时,编译器也需要知道什么样的数据将被存储在内存位置。比如会是数字还是字母?如果变量将存储数字,那么一个数字可以有多大?变量是存储小数还是只存储整数?您可以通过给变量分配一个 数据类型来回答这些问题。例如,登录计数器只需要保存正整数。下面的代码演示了如何在 C# 中用整数数据类型声明名为counter的变量:
int counter;
指定数据类型被称为 强类型。强类型导致更有效的内存管理、更快的执行和编译器类型检查,所有这些都减少了运行时错误。运行时错误是程序运行时可能发生的错误,在您构建应用时,编译器不会捕捉到这些错误。例如,由于在计算中使用了错误的数据类型,可能会出现不正确的舍入。
一旦声明了变量,就需要在单独的语句中或者在声明语句本身中给它赋一个初始值。例如,下面的代码:
int counter = 1;
相当于这样:
int counter;
counter = 1;
了解基本数据类型
C# 支持基本数据类型,如数字、字符和日期类型。
整数数据类型
整数数据类型仅表示整数。表 A-1 总结了 C# 中使用的整型数据类型。
表 A-1 整数数据类型
| 数据类型 | 存储大小 | 取值范围 |
|---|---|---|
Byte | 8 位 | 0 到 255 |
Short | 16 位 | –32768 到 32767 |
Integer | 32 位 | –2147483648 至 2147483647 |
Long | 64 位 | -9,223,372,036,854,775,808 至 9,223,372,036,854,775,807 |
显然,在为变量选择数据类型时,内存大小很重要。一个不太明显的考虑是编译器处理数据类型的容易程度。编译器对整数 执行算术运算比其他类型更有效。通常,最好使用整数作为计数器变量,即使一个字节 或短类型可以轻松管理达到的最大值。
非整数数据类型
如果变量将存储包含小数部分 的数字,那么您必须使用非整数数据类型。C# 支持表 A-2 中列出的非整数数据类型。
表 A-2。非整数数据类型
| 数据类型 | 存储大小 | 取值范围 |
|---|---|---|
Single | 32 位 | 负值为–3.4028235 e+38 到–1.401298 e–45;正值为 1.401298 e–45 到 3.4028235E+38 |
Double | 64 位 | 1.79769313486231570E+308 到–4.94065645841246544 e–324 为负值;正值为 4.94065645841246544 e–324 到 1.79769313486231570E+308 |
Decimal | 128 位 | 0 到+/-79,228,162,514,264,337,593,543,950,335,不带小数点;0 到+/-7.9228162514264337593543950335,小数点右边有 28 位 |
与单个 或双 数据类型相比,十进制数据类型包含更多的有效数字,并且不受舍入误差的影响。十进制数据类型通常用于需要更高精度的金融或科学计算。
字符数据类型
字符数据类型用于保存人类语言中使用的字符的变量。例如,字符数据类型保存字母(如“a”)或用于显示和打印的数字(如“2 个苹果”)C# 中的字符数据类型是基于 Unicode 的,Unicode 定义了一个字符集,可以表示从英语到阿拉伯语和中文普通话等各种语言中的字符。C# 支持两种字符数据类型:char和string。char数据类型保存单个(16 位)Unicode 字符值,如a或B。字符串数据类型包含一系列 Unicode 字符。它的范围从 0 到大约 20 亿个字符。
布尔数据类型
布尔数据类型包含一个 16 位值,该值被解释为 true 或 false。它用于只能是两个值之一的变量,例如是或否,或开或关。
日期数据类型
日期以 64 位整数保存,其中每个增量代表从公历开始(1/1/0001 12:00a.m)经过的一段时间。).
对象数据类型
object 数据类型是一个 32 位地址,指向另一种数据类型的内存位置。当变量引用的实际数据类型直到运行时才能确定时,通常使用它来声明变量。尽管 object 数据类型可以是引用其他数据类型的总称,但就性能而言,它是最低效的数据类型,除非绝对必要,否则应该避免使用。
可空的 类型
默认情况下,不能为布尔值、整数和 double 数据类型等值类型分配空值。当从数据结构中检索数据时,比如一个允许空值的数据库,这可能会成为问题。当声明一个可能被赋值为 null 的值类型变量时,通过将问号(?)在类型名之后,像这样:
double salary = null; // Not allowed.
double? salary = null; // allowed.
介绍复合 数据类型
组合基本数据类型可以创建复合数据类型。结构、数组和类是复合数据类型的例子。
构筑物
当您想要组织和处理不需要类方法和构造函数开销的信息时,结构数据类型非常有用。它非常适合表示轻量级对象,如点或矩形的坐标。structure 类型的单个变量可以存储这样的信息。你用关键字struct声明一个结构。例如,下面的代码创建了一个名为Point的结构来存储二维表面中点的坐标:
public struct Point
{
public int _x, _y;
public Point(int x, int y)
{
_x = x;
_y = y;
}
}
一旦定义了结构,就可以声明结构类型的变量并创建该类型的新实例,如下所示:
Point p1 = new Point(10,20);
数组
数组通常用于组织和处理相同数据类型的组;例如,您可能需要处理一组名称,因此您可以通过在变量名后紧跟方括号([])来声明数组数据类型,如下所示:
string[] name;
new操作符用于创建数组,并将其元素初始化为默认值。因为数组的元素由从零开始的索引引用,所以下面的数组包含五个元素:
string[] name = new string[4];
要在声明数组时初始化数组元素,可以使用花括号({})列出值。因为可以推断出数组的大小,所以不必声明它。
string[] name = {"Bob","Bill","Jane","Judy"};
C# 支持多维数组。声明数组时,用逗号分隔维度的大小。以下声明创建一个五行四列的二维整数数组:
string[,] name = new string[4,3];
要在声明二维数组时初始化该数组的元素,可以在花括号中使用花括号来列出数组元素。
int[,] intArray = {{1,2}, {3,4}, {5,6}, {7,8}};
您可以使用数组名称后跟括号中元素的索引来访问数组元素。第一个元素的索引编号从 0 开始。例如,name[2]引用先前声明的 names 数组的第三个元素,其值为Jane。
类类类
在面向对象的编程语言中,类被广泛使用。本书的大部分内容是关于它们的创建和使用。至此,可以说类为对象定义了一个复杂的数据类型。它们包含关于对象应该如何行为的信息,包括其名称、方法、属性和事件。那个 .NET Framework 包含许多您可以使用的预定义类。您也可以创建自己的类类型定义。定义为类类型的变量包含一个指向对象内存位置的地址指针。下面的代码声明了在 .NET 框架:
StringBuilder sb = new StringBuilder();
查看文字、常量和枚举
尽管变量值在程序执行过程中会发生变化,但文字和常量包含不变的数据项。
文字
文字是隐式分配了数据类型的固定值,通常用于初始化变量。以下代码使用文字将2的值与整数值相加:
Count = Count + 2;
通过检查文本,编译器将数据类型分配给文本。没有十进制值的数值被指定为整数数据类型;具有十进制值的数据被指定为双精度数据类型。关键字true和false被指定为布尔数据类型。如果文字包含在引号中,则它被指定为字符串数据类型。在下面的代码行中,两个字符串文字被组合并赋给一个字符串变量:
FullName = "Bob" + "Smith";
通过向文本追加一个类型字符,可以覆盖文本的默认数据类型赋值。例如,12.25的值将被分配双精度数据类型,而12.25f的值将导致编译器为其分配单数据类型。
常数
很多时候,您必须在代码中重复使用同一个常量值。例如,一系列几何计算可能需要使用圆周率的值。不用在你的代码中重复文字 3.14,你可以通过使用一个声明的常量来使你的代码更可读和可维护。使用关键字const声明一个常量,后跟数据类型和常量名称:
const Single pi = 3.14159265358979323846;
常量在声明时被赋值,这个值不能被修改或重新赋值。
枚举
您经常需要将变量值赋给几个相关的预定义常量之一。在这些情况下,您可以创建枚举类型来对值进行分组。枚举将一组整数常量与可在代码中使用的名称相关联。例如,下面的代码创建了一个类型为Manager的enum,用于定义三个相关的管理器常量,它们的名称分别为DeptManager、GeneralManager和AssistantManager,值分别为0、1和2:
enum Manager
{
DeptManager,
GeneralManager,
AssistantManager,
}
可以声明一个enum类型的变量,并将其设置为一个enum常量。
Manager managerLevel = Manager.DeptManager;
注此 .NET Framework 提供了各种内部常数和枚举,旨在使您的编码更加直观和易读。例如,
StringAlignment枚举指定文本字符串相对于其布局矩形的对齐方式。
探索变量范围
变量的两个重要方面是它的范围和生存期。变量的作用域指的是如何从其他代码中访问该变量。变量的生存期是变量有效并可供使用的时间段。变量的作用域和生存期由声明变量的位置和用于声明变量的访问修饰符决定。
块级范围
代码块是一组分组的代码语句。代码块的例子包括以if - else、do - loop或for - next语句组织的代码。块级范围是变量可以拥有的最窄范围。在代码块中声明的变量只能在声明它的代码块中使用。在下面的代码中,变量blockCount只能从 if 块内部访问。任何访问块外变量的尝试都会产生编译器错误。
if (icount > 10)
{
int blockCount;
blockCount = icount;
}
程序范围
过程是可以从其他代码中调用和执行的代码块。C# 支持两种类型的过程:方法和属性。在代码块外但在过程内声明的变量具有过程级范围。具有过程范围的变量可以由同一过程中的代码访问。在下面的代码中,计数器iCount是用过程范围声明的,可以从Counter方法的过程块中的任何地方引用:
void Counter()
{
int iCount = 0;
do
{
iCount = iCount + 2;
}
while (iCount < 10);
}
过程范围变量的生存期限于过程执行的持续时间。
模块范围
具有模块范围的变量可用于类或结构中的任何代码。为了具有模块范围,变量在类或结构的一般声明部分(在任何过程块之外)声明。为了限制声明它的模块的可访问性,可以使用private访问修饰符关键字。在下面的代码中,iCount变量可以被类中定义的两个过程访问:
public class Class1
{
private int _iCount;
public void IncrementCount()
{
int iCount = 0;
do
{
iCount = iCount + 2;
}
while (iCount < 10);
}
public void ReadCount()
{
Console.WriteLine(_iCount.ToString());
}
}
用模块范围声明的变量的生存期与声明该变量的类或结构的对象实例的生存期相同。
注关于范围的进一步讨论,见第六章。
了解数据类型转换
在程序执行过程中,很多时候都必须将一个值从一种数据类型转换为另一种数据类型。数据类型之间的转换过程称为转换或转换。
隐式转换
C# 编译器会自动为您执行一些数据类型转换。对于数值类型,当要存储的值可以放入变量而不被截断或舍入时,可以进行隐式转换。例如,在下面的代码中,整数数据类型被隐式转换为 long 数据类型:
int i1 = 373737373;
long l1 = i1;
l1 *= l1;
显式转换
显式转换被称为强制转换。若要执行强制转换,请在要转换的值或变量前面的括号中指定要强制转换的类型。以下代码使用强制转换将 double 类型n1显式转换为 integer 类型:
double n1 = 3.73737373;
int i1 = (int)n1;
扩大和缩小转换
当要转换的数据类型可以容纳原始数据类型中包含的所有可能值时,就会发生扩大转换 。例如,整数数据类型可以转换为双精度数据类型,而不会有任何数据丢失或溢出。当数字被截断时,就会发生数据丢失。例如,如果将 2.54 转换为整数数据类型,它将被截断为 2。当数字太大而不适合新的数据类型时,就会发生溢出。例如,如果将数字 50000 转换为 short 数据类型,则超出了 short 数据类型的最大容量,从而导致溢出错误。另一方面,收缩转换 发生在被转换的数据类型不能容纳原始数据类型中可以包含的所有值时。例如,当 double 数据类型的值转换为 short 数据类型时,原始值中包含的任何十进制值都将丢失。此外,如果原始值超过短数据类型的限制,将会发生运行时异常。在代码中实现收缩转换时,应该特别小心地捕捉这些情况。
使用运算符
运算符是一个代码符号,它告诉编译器对一个值执行运算。运算可以是算术、比较或逻辑。
算术运算符
算术运算符对数值类型执行数学操作。表 A-3 列出了 C# 中常用的算术运算符。
表 A-3。算术运算符
| 操作员 | 描述 |
|---|---|
= | 分配 |
* | 增加 |
/ | 分开 |
+ | 添加 |
- | 减法 |
下面的代码将整数数据类型的值递增 1:
Count = Count + 1;
C# 还支持将赋值和操作结合起来的速记赋值操作符。以下代码等效于前面的代码:
Count += 1;
如果你要增加 1,你也可以使用简写赋值++。以下代码等效于前面的代码:
Count ++;
比较运算符
比较运算符比较两个值,并返回布尔值true或false。表 A-4 列出了 C# 中常用的比较运算符。
表 A-4。比较运算符
| 操作员 | 描述 |
|---|---|
< | 不到 |
<= | 小于或等于 |
> | 大于 |
>= | 大于或等于 |
== | 等于 |
!= | 不等于 |
您可以在条件语句中使用比较运算符来决定何时执行代码块。在显示消息之前,下面的if块检查无效登录尝试的次数是否大于三次:
if (_loginAttempts > 3)
{
Messagebox.Show("Invalid login.");
}
逻辑运算符
逻辑运算符组合条件运算符的结果。三种最常用的逻辑运算符是 AND、OR 和 NOT 运算符。 AND 运算符(&&)组合两个表达式,如果两个表达式都为真,则返回true。 OR 运算符(||)组合两个表达式,如果其中一个为真,则返回true。 NOT 运算符(!)切换比较结果:值true返回false,值false返回true。以下代码在运行方法之前检查登录用户是部门经理还是助理经理:
if (currentUserLevel == Manager.AssistantManager ||
currentUserLevel == Manager.DeptManager)
{
ReadLog();
}
三进制运算符
三元运算符计算布尔表达式,并根据表达式的结果返回两个值之一。下面显示了三元运算符的语法:
condition ? first_expression : second_expression;
如果条件评估为 true,则返回第一个表达式的结果。如果条件评估为 false,则返回第二个表达式的结果。下面的代码检查 x 的值是否为零。如果是,则返回 0;如果不是,它将 y 除以 x 并返回结果。
return x == 0.0 ? 0 : y/x;
介绍决策结构
决策结构允许根据条件语句的评估有条件地执行代码块。if语句评估一个布尔表达式,如果结果为真,则执行代码块。switch语句检查同一个表达式的几个不同值,并根据结果有条件地执行一个代码块。
如果语句
若要在条件为真时执行代码块,请使用以下结构:
if (condition1)
{
//code
}
若要在条件为真时执行代码块,在条件为假时执行替代代码块,请添加一个 else 块。
if (condition1)
{
//code
}
else
{
//code
}
如果第一个评估为false,要测试附加条件,添加一个else - if模块:
if (condition1)
{
//code
}
else if (condition2)
{
//code
}
else
{
//code
}
一条if语句可以有多个-else-if块。如果条件评估为true,则执行相应的代码语句,之后执行跳转到语句的末尾。如果条件评估为false,则检查下一个 else-if 条件。else 块是可选的,但是如果包含,它必须是最后一个。else模块没有条件检查,仅当所有其他条件检查评估为false时才执行。下面的代码演示了如何使用if语句来评估一系列条件。它检查绩效评级以确定使用何种奖金,并检查员工是否是经理以确定最低奖金。
if (performance ==1)
{
bonus = salary * 0.1;
}
else if (performance == 2)
{
bonus = salary * 0.08;
}
else if (employeeLevel == Manager.DeptManager)
{
bonus = salary * 0.05;
}
else
{
bonus = salary * 0.03;
}
切换报表
虽然switch语句类似于if - else语句,但它用于测试一系列值的单个表达式。switch语句的结构如下:
switch (expression)
{
case 1:
Console.WriteLine("Case 1");
break;
case 2:
Console.WriteLine("Case 2");
break;
default:
Console.WriteLine("Default case");
break;
}
一个switch语句可以有多个case块。如果测试表达式的值与case表达式匹配,那么case块中的代码语句就会执行。在case块执行之后,您需要一个break语句来绕过其余的 case 语句。如果测试表达式与case表达式不匹配,执行跳转到下一个case块。default块没有表达式。如果没有执行其他 case 块,它就会执行。default块是可选的,但如果使用,它必须是最后一个。以下示例使用switch来评估绩效评级,以设置适当的奖金率:
switch(performance)
{
case 1:
bonus = salary * 0.1;
break;
case 2:
bonus = salary * 0.08;
break;
case 3:
bonus = salary * 0.03;
break;
default:
bonus = salary * 0.01;
break;
}
使用循环结构
循环结构重复一段代码,直到满足一个条件。C# 支持以下循环结构。
而声明
当布尔表达式保持不变时,while语句重复执行代码。表达式在循环开始时计算。下面的代码执行,直到一个有效的登录变量评估为true:
while (validLogin == false)
{
//code statements...
}
Do-While 语句
do - while循环类似于while循环,只是表达式是在循环结束时计算的。以下代码将循环运行,直到达到最大登录尝试次数:
do
{
//code statements...
}
while (iCount < maxLoginAttempts);
对于声明
一条for语句根据存储在计数器中的值在一个代码块中循环特定的次数。当您知道循环在设计时需要执行的次数时,For 语句是更好的选择。在for语句后面的括号中,初始化一个计数器,定义求值表达式,并定义计数器的增量。
for (int i = 0; i < 10; i++)
{
//Code statements...
}
对于每一个语句
for - each语句循环遍历集合中每一项的代码。一个集合 是一组有序的条目;例如,放置在 Windows 窗体上的控件被组织到一个控件集合中。要使用for - each语句,首先声明集合中包含的条目类型的变量。此变量设置为集合中的当前项。下面的for - each语句遍历雇员列表集合中的雇员:
foreach (Employee e in employeeList)
{
//Code statements
}
如果需要有条件地退出循环代码块,可以使用break语句。下面的代码显示了如何跳出for - each循环:
foreach (Employee e in employeeList)
{
//Code statements
if (e.Name == "Bob")
{
break;
}
}
介绍方法
方法是可以从其他代码中调用和执行的代码块。将应用分解成离散的逻辑代码块极大地增强了代码的维护和重用。C# 支持返回值的方法和不返回值的方法。当您声明一个方法时,您为该方法指定一个访问修饰符、一个返回类型和一个名称。下面的代码声明了一个没有返回类型(由关键字指定)的方法,用于记录事件日志的登录:
public void RecordLogin(string userName)
{
EventLog appLog = new EventLog();
appLog.Source = "OSO App";
appLog.WriteEntry(userName + " has logged in.");
}
您可以使用参数列表来声明方法,该列表定义了调用方法时必须传递给该方法的参数。以下代码定义了一个封装奖金分配的方法。调用代码将一个整数类型的值传递给该方法,并接收一个double类型的值。
public double GetBonusRate(int performanceRating)
{
double bonusRate;
switch (performanceRating)
{
case 1:
bonusRate = 0.1;
break;
case 2:
bonusRate = 0.08;
break;
case 3:
bonusRate = 0.03;
break;
default:
bonusRate = 0.01;
break;
}
return bonusRate;
}
下面的代码演示了如何调用方法:
double salary;
int performance = 0;
double bonus = 0;
// Get salary and performance data from data base...
bonus = GetBonusRate(performance) * salary;
如果该方法的访问修饰符是私有的,则只能从同一类中的代码访问它。如果该方法需要被其他类中的代码访问,那么使用public访问修饰符。
本附录中的信息旨在帮助您快速了解一些基本的编程概念。您现在应该更熟悉 C# 语法,并准备通过阅读本书的其余部分来扩展和构建这些概念。
十八、附录二:C# 中的异常处理
这里讨论的主题扩展了在第八章中发现的异常处理的讨论,所以这个讨论假设你已经彻底地回顾了第八章中的。本附录的目的是回顾微软关于异常管理的建议,并介绍 .NET 框架。
管理异常
当违反了您的编程逻辑所做的隐式假设时,就会生成异常。例如,当一个程序试图连接到一个数据库时,它假定数据库服务器已经启动并正在网络上运行。如果找不到服务器,就会产生一个异常。应用优雅地处理任何可能发生的异常是很重要的。如果没有处理异常,您的应用将会终止。
你应该在你的方法中加入一个系统的异常处理过程。为了方便这一过程 .NET 框架通过try、catch和finally代码块利用结构化异常处理。第一步是检测代码执行时可能引发的任何异常。要检测任何抛出的异常,将代码放在 try块中。当在try块中抛出异常时,执行转移到catch块。您可以使用多个catch块来过滤可能抛出的特定类型的异常。 finally块执行您希望执行的任何清理代码。无论是否抛出异常,finally块中的代码都会执行。下面的代码演示如何使用适当的异常处理结构从文件中读取名称列表:
public ArrayList GetNames(string file)
{
try
{
using( StreamReader stream = new StreamReader())
{
ArrayList names = new ArrayList();
while (stream.Peek() > -1)
{
names.Add(stream.ReadLine());
}
}
}
catch (FileNotFoundException e)
{
//Could not find file
}
catch (FileLoadException e)
{
//Could not open file
}
catch (Exception e)
{
//Some kind of error occurred. Report error.
}
finally
{
stream.Close();
}
return names;
}
捕捉到异常后,流程的下一步是确定如何对其做出响应。您基本上有两种选择:要么从异常中恢复,要么将异常传递给调用过程。以下代码演示了如何通过将结果设置为零来从 DivideByZeroException中恢复:
...
try
{
Z = x / y;
}
catch (DivideByZeroException e)
{
Z = 0;
}
...
使用 throw语句将异常传递给调用过程。下面的代码演示如何将异常引发到调用过程,在调用过程中可以捕获和处理该异常:
catch (FileNotFoundException e)
{
throw e;
}
随着异常沿着调用链向上抛出,原始异常的相关性会变得不那么明显。为了保持相关性,您可以将异常包装在一个新的异常中,该异常包含增加异常相关性的附加信息。下面的代码演示如何将捕获的异常包装在新的异常中,然后沿调用链向上传递它:
catch (FileLoadException e)
{
throw new Exception("GetNames function could not open file", e);
}
您通过使用Exception类的InnerException属性 来保留原始异常。
catch (FileLoadException e)
{
throw new Exception("Could not open file. " + e.InnerException, e);
}
在应用的各种方法中一致地实现这种异常管理策略,将极大地增强您构建高度可维护、灵活和成功的应用的能力。
使用 .NET 框架异常类
公共语言运行时(CLR) 有一组内置的异常类。如果在执行代码指令时发生错误,CLR 将引发适当异常类型的对象实例。全部 .NET Framework 异常类派生自SystemException类,而后者又派生自Exception类。这些基类提供了所有异常类所需的功能。
框架中的每个名称空间都包含一组从SystemException类派生的异常类。这些异常类处理在实现命名空间中包含的功能时可能发生的常见异常。为了实现健壮的异常处理,熟悉各种名称空间提供的异常类非常重要。例如,表 B-1 总结了System.IO名称空间中的异常类。
表 B-1。System.IO名称空间中的异常类
| 例外 | 描述 |
|---|---|
IOException | 使用流、文件和目录访问信息时引发的异常的基类。 |
DirectoryNotFoundException | 当文件或目录的一部分找不到时抛出。 |
EndOfStreamException | 当试图读取超过流的结尾时抛出。 |
FileLoadException | 当找到文件但无法加载时抛出。 |
FileNotFoundException | 当试图访问磁盘上不存在的文件失败时抛出。 |
PathTooLongException | 当路径或文件名超过系统定义的最大长度时抛出。 |
中的每个异常类 .NET Framework 包含表 B-2 中列出的属性。这些属性有助于识别异常发生的位置及其原因。
表 B-2。异常类属性
| 属性 | 描述 |
|---|---|
Message | 获取描述当前异常的消息。 |
Source | 获取或设置导致错误的应用或对象的名称。 |
StackTrace | 获取引发当前异常时调用堆栈上帧的字符串表示形式。 |
InnerException | 获取导致当前异常的异常实例。 |
HelpLink | 获取或设置与此异常关联的帮助文件的链接。 |
此外,异常类的 ToString方法提供了当前异常的摘要信息。它结合了抛出当前异常的类的名称、消息、调用内部异常的ToString方法的结果以及当前异常的堆栈跟踪信息。
您会发现 .NET Framework 为您提供了处理应用中可能出现的大多数异常的能力。在需要实现自定义错误处理的情况下,可以创建自己的异常类。这些类需要从System.ApplicationException继承,而后者又从System.Exception继承。创建自定义异常类是一个高级主题,因此超出了本文的范围;有关更多信息,请参考。位于http://msdn.microsoft.com/en-us/library/的. NET 框架文档。
使用的重要性
当您编写应用时,很多时候您必须从 C# 托管类型访问非托管资源。例如,您可能需要读写一个文本文件,或者打开一个到数据库的连接。在使用完这些资源后,尽快释放它们是很重要的。如果您等待垃圾收集器来清理资源,它们可能会长时间不可访问,从而阻止其他程序访问这些资源。发生这种情况的一个场景是连接到数据库。可以建立的连接数量有限。如果这些连接没有得到正确的管理,那么性能将会受到很大的影响。
为了安全地释放任何非托管资源,访问这些资源的类必须实现IDispose接口和Dispose方法。向该类的用户保证,调用Dispose方法将正确清理资源。例如,当您完成一个数据库连接时,您应该在一个finally块中调用它的Dispose方法,如下面的代码所示。
SqlConnection pubConnection = new SqlConnection();
string connString;
try
{
connString = "Data Source=drcsrv01;Initial Catalog=pubs;Integrated Security=True";
pubConnection.ConnectionString = connString;
pubConnection.Open();
//work with data
}
catch (SqlException ex)
{
throw ex;
}
finally
{
pubConnection.Dispose();
}
这种模式对于编写可靠的代码非常重要,以至于微软实现了一种方便的语法来确保正确使用IDisposable对象。当您使用using语句时,您要确保当对象超出范围或者当您调用对象的方法时发生异常时,正确地调用了Dispose方法。下面的代码等同于前面显示的用于创建和使用SqlConnection的代码。
string connString = "Data Source=drcsrv01;Initial Catalog=pubs;Integrated Security=True";
using(SqlConnection pubConnection = new SqlConnection())
{
pubConnection.ConnectionString = connString;
pubConnection.Open();
//work with data
}
您应该养成为任何实现了IDispose接口的类使用 using 模式的习惯。下面的代码使用这个模式,通过使用StreamReader类来打开和读取一个文件。
string path = @"c:\temp\MyTest.txt";
using (StreamReader sr = File.OpenText(path))
{
string s = "";
while ((s = sr.ReadLine()) != null)
{
// work with text
}
}
本讨论为您提供了对第八章的补充,涵盖了有效处理异常和使用 .NET Framework 异常类。请参见第八章对该主题的基本讨论。
十九、附录三:安装所需的软件
我在这本书里包含了许多学习活动。为了从我讨论的主题中获得最大的收获,你应该完成这些活动。这是理论具体化的地方。我希望你能认真对待这些活动,并彻底完成它们,甚至反复完成。
第一部分中的 UML 建模活动是为使用 UMLet 的人准备的。我选择这个程序是因为它是一个很好的学习图表的工具。它使您能够创建 UML 图,而无需添加许多高级功能。 UMLet 是一个免费的开源工具,可以从http://www.umlet.com下载。但是你不需要一个工具来完成这些活动;纸和铅笔就可以了。
第二部分中的活动需要安装了 C# 的 Visual Studio 2012。您可以使用免费版的 Visual Studio 2012 Express,也可以使用试用版的 Visual Studio 2012 Professional。这些版本在http://msdn.microsoft.com/en-us/vstudio/有售。我鼓励你安装帮助文件,并在完成活动时充分利用它们。
第三部分中的活动需要 Microsoft SQL Server 2008 R2 或 SQL Server 2012。您可以使用免费版本的 SQL Server Express 或在http://msdn.microsoft.com/en-us/sqlserver/提供的 SQL Server 试用版。安装 SQL Server 时,请确保将自己添加为管理员。
安装示例数据库
安装本书中使用的示例数据库的脚本可以从http://www.apress.com/9781430249351下载(向下滚动并点击源代码选项卡)。为了安装脚本,请按照下列步骤操作:
-
打开命令提示符窗口。
-
在命令提示符下,使用 cd 命令导航到包含示例数据库脚本的文件夹。
cd c:\SampleDatabases -
运行
SQLCmd.exe,指定instOSODB.sql为输入文件。 -
要在默认实例上安装数据库,请使用
SQLCmd.exe -E -i instOSODB.sql -
要在命名实例上安装数据库,请使用
SQLCmd.exe -E -S ComputerName\InstanceName -i instOSODB.sql -
对
instpubs.sql和instnwnd.sql文件重复上述步骤。
注意您也可以使用 SQL Server Management Studio 来创建数据库。
验证数据库安装
要验证数据库安装:
-
Start Visual Studio. If you don’t see the Server Explorer window shown in Figure C-1, open it by choosing Server Explore on the View menu.
图 C-1 。服务器资源管理器窗口
-
In the Server Explorer window, right-click the Data Connections node and select Add Connection. In the Add Connections dialog box shown in Figure C-2, fill in the name of your server, select the Northwind database, and click OK.
图 C-2 。“添加连接”对话框
-
Expand the Northwind database node and the Tables node in the Database Explorer window, as shown in Figure C-3.
图 C-3 。展开“表”节点
注意如果你正在使用 SQLExpress 你需要使用服务器名和实例名(LocalHost\SQLExpress)来连接。
-
Right-click the Suppliers table node and select Show Table Data. The Suppliers table data should display as shown in Figure C-4.
图 C-4 。查看表格数据
-
重复这些步骤来测试
pubs和OfficeSupply数据库。测试后,退出 Visual Studio。