导言
一般控件(比如GridView)显示数据的时候对数据只能读取,而需要处理数据的功能是非常常见的.典型的情况是为每行数据添加一个Button, LinkButton, 或ImageButton . 当点击这些button时,数据会PostBack,执行一些服务器端的代码. 一条条的编辑或删除数据是最常见的情况.实际上,编辑和删除是如此常见,从概述插入、更新和删除数据 开始, 我们可以看到GridView, DetailsView, 和 FormView可以零代码的完成这些功能.
除了编辑和删除button,GridView, DetailsView, and FormView 也可以包含一些执行自定义服务器端代码的Buttons, LinkButtons, 或 ImageButtons .在这一章我们来看看如何向一个GridView 或 DetailsView 里添加自定义的button.我们还将创建一个根据supplier进行分页的页面.对每个给定的supplier,FormView会显示它的相关信息,外加一个Button .点击这个Button 时,所有相关products会被标记为停止使用.另外,GridView 会列出选定的supplier提供的所有product ,并且每一行会包含“Increase Price”和“Discount Price”两个Button.这两个Button用来提高或降低10%的product单价(见图一).
图 1: FormView和GridView 都包含了执行自定义行为的Button
第一步: 添加一个Button 教程页
在研究如何添加自定义button之前,我们先花一点时间在网站里创建一些页,这些页会在本指南里用到.先添加一个名为CustomButtons的文件夹,然后添加如下的两个页.添加页的时候确保每页都选择了Site.master作为母板页.
Default.aspx
CustomButtons.aspx
图 2: 添加本指南需要的页面
象其它文件夹一样,CustomButtons 文件夹里的Default.aspx 用来列出教程章节.记得SectionLevelTutorialListing.ascx 这个用户控件提供了这个功能.因此,从解决方案浏览里将这个用户控件拖到页面上.
图 3: 添加SectionLevelTutorialListing.ascx 用户控件 到Default.aspx
最后,将这些页的地址加到 Web.sitemap 的条目里.在Paging and Sorting <siteMapNode>之后添加下面的标记.
1
2
3
4
5
6
7
8
9
10
11
|
<siteMapNode
title= "Adding Custom Buttons"
description="Samples of Reports that Include Buttons for Performing
Server-Side Actions"
url= "~/CustomButtons/Default.aspx" >
<siteMapNode
title= "Using ButtonFields and Buttons in Templates"
description="Examines how to add custom Buttons, LinkButtons,
or ImageButtons as ButtonFields or within templates."
url= "~/CustomButtons/CustomButtons.aspx" />
</siteMapNode>
|
修改完Web.sitemap后,在浏览器里看一下本教程站点,现在左边的菜单里包含了编辑,插入,删除教程的项.
图 4: Site Map包含了添加自定义button教程
第二步: 添加一个列出 Supplier的FormView
我们首先来添加一个列出suppliers的FormView .正如在导言里讨论的那样,FormView根据supplier分页,并在GridView显示supplier 提供的所有product .另外FormView 会包含一个Button .当点击时,所有相关products会被标记为停止使用.在我们为FormView添加自定义button之前,我们首先创建显示supplier 信息的FormView .
打开CustomButtons文件夹里的CustomButtons.aspx 页,从工具箱里拖一个FormView进来,将FormView的ID设置为Suppliers.打开FormView的智能标签,创建一个名为SuppliersDataSource的ObjectDataSource.
图 5: 创建一个名为SuppliersDataSource的ObjectDataSource
选择SuppliersBLL 类的GetSuppliers()方法配置ObjectDataSource(见图6).由于这个FormView没有提供修改supplier 信息的界面,所以在UPDATE 标签的下拉列表里选择None.
图 6: 使用 SuppliersBLL 类的GetSuppliers() 方法配置数据源
数据源配置完成后,Visual Studio会生成一个InsertItemTemplate,一个EditItemTemplate和一个FormView的ItemTemplate.去掉InsertItemTemplate 和EditItemTemplate ,修改ItemTemplate,让它只显示supplier的公司名,电话号码.最后,在智能标签里选中Enable Paging checkbox 或者设置AllowPaging 属性为True.完成这些后,你的声明标记看起来应该和以下差不多:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
<asp:FormView ID= "Suppliers" runat= "server" DataKeyNames= "SupplierID"
DataSourceID= "SuppliersDataSource" EnableViewState= "False" AllowPaging= "True" >
<ItemTemplate>
<h3>
<asp:Label ID= "CompanyName" runat= "server"
Text= '<%# Bind("CompanyName") %>' />
</h3>
<b>Phone:</b>
<asp:Label ID= "PhoneLabel" runat= "server" Text= '<%# Bind("Phone") %>' />
</ItemTemplate>
</asp:FormView>
<asp:ObjectDataSource ID= "SuppliersDataSource" runat= "server"
OldValuesParameterFormatString= "original_{0}"
SelectMethod= "GetSuppliers" TypeName= "SuppliersBLL" >
</asp:ObjectDataSource>
|
图 7: FormView列出当前选定的Supplier的CompanyName and Phone
第三步 : 添加一个GridView,用来列出某个Supplier的所有Product
在添加“Discontinue All Products”Button 前,先在FormView 下面添加一个GridView . 设置ID 为SuppliersProducts,添加一个名为SuppliersProductsDataSource的ObjectDataSource .
图 8: 创建一个名为SuppliersProductsDataSource的ObjectDataSource
选择ProductsBLL 类的GetProductsBySupplierID(supplierID)方法配置ObjectDataSource(见图9).虽然GridView 允许修改product的价格,但是并不使用的GridView自带的编辑或删除功能.因此在UPDATE, INSERT, and DELETE 标签的下拉列表里都选择None.
图 9: 使用ProductsBLL 类的GetProductsBySupplierID(supplierID) 方法配置数据源
由于GetProductsBySupplierID(supplierID)有一个输入参数,ObjectDataSource向导会提示我们配置这个参数.为了将SupplierID 从FormView传过来,在参数来源的下来列表里选择Control,在ControlID 下拉列表里选择Suppliers (在第二步里创建的FormView 的ID).
图 10: 指定 supplierID 参数的来源为Suppliers FormView
完成了ObjectDataSource 向导后,GridView 里的每一行product会包含一个BoundField 和一个CheckBoxField . 我们来精简一下,只显示Discontinued CheckBoxField,ProductName 和UnitPrice .我们修改UnitPrice 列的格式为货币. 你的GridView 和SuppliersProductsDataSource ObjectDataSource的声明标记看起来应该和下面差不多:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
<asp:GridView ID= "SuppliersProducts" AutoGenerateColumns= "False"
DataKeyNames= "ProductID" DataSourceID= "SuppliersProductsDataSource"
EnableViewState= "False" runat= "server" >
<Columns>
<asp:BoundField DataField= "ProductName" HeaderText= "Product"
SortExpression= "ProductName" />
<asp:BoundField DataField= "UnitPrice" HeaderText= "Price"
SortExpression= "UnitPrice" DataFormatString= "{0:C}"
HtmlEncode= "False" />
<asp:CheckBoxField DataField= "Discontinued" HeaderText= "Discontinued"
SortExpression= "Discontinued" />
</Columns>
</asp:GridView>
<asp:ObjectDataSource ID= "SuppliersProductsDataSource" runat= "server"
OldValuesParameterFormatString= "original_{0}"
SelectMethod= "GetProductsBySupplierID" TypeName= "ProductsBLL" >
<SelectParameters>
<asp:ControlParameter ControlID= "Suppliers" Name= "supplierID"
PropertyName= "SelectedValue" Type= "Int32" />
</SelectParameters>
</asp:ObjectDataSource>
|
现在我们的显示了一个主/从表,用户通过在上面的FormView 里选择一个supplier ,在下方的GridView 里就可以看到这个supplier 提供的products.
图11是在FormView里选择Tokyo Traders supplier 的截图.
图 11: 在GridView显示选定的Supplier的产品
第四步: 创建DAL和BLL层的停止使用Supplier的所有Products 的方法
在FormView 添加discontinue button前,我们首先需要在DAL 和BLL 里添加完成这个功能的方法.这个方法的名字为DiscontinueAllProductsForSupplier(supplierID). 当点击FormView的Button 时,我们会调用Business Logic Layer里的这个方法,并将选定的supplier的SupplierID传进去.BLL 会继续调用Data Access Layer的相关方法,这个方法会向数据库提交一个停止使用选定的supplier的products的UPDATE语句
象在以前的教程里所做的那样,我们使用自底向上的方法,首先创建DAL 的方法,然后是BLL ,最后在 ASP.NET page里实现这个功能.打开App_Code/DAL文件夹里的Northwind.xsd ,为ProductsTableAdapter 添加一个新方法(右键点击ProductsTableAdapter ,选择Add Query).这样弹出TableAdapter Query 的配置向导.首先指定DAL 需要使用的SQL .
图 12: 使用SQL Statement创建DAL 方法
接着,向导会询问我们创建哪种类型的query .由于DiscontinueAllProductsForSupplier(supplierID)需要更新Products表,为指定的supplierID 的所有products的Discontinued 字段设置为1,因此我们需要创建一个更新数据的query .
图 13: 选择UPDATE Query的类型
下一个向导显示的窗口提供了TableAdapter的已经存在的UPDATE 语句,它会updates 在Products DataTable定义的所有的字段.用下面的语句替换它:
UPDATE [Products] SET
Discontinued = 1
WHERE SupplierID = @SupplierID
输入以上语句后点Next,最后一个向导窗口需要输入该方法的名字—DiscontinueAllProductsForSupplier.完成向导后点Finish button.当你回到DataSet 设计器时你应该可以在ProductsTableAdapter 看到名为DiscontinueAllProductsForSupplier(@SupplierID)的方法.
图 14: 为 DAL 的方法取名为 DiscontinueAllProductsForSupplier
完成Data Access Layer里的DiscontinueAllProductsForSupplier(supplierID)方法后,我们下一步的任务是创建Business Logic Layer里的相应的方法.打开ProductsBLL 类文件,添加以下内容:
1
2
3
4
|
public int DiscontinueAllProductsForSupplier( int supplierID)
{
return Adapter.DiscontinueAllProductsForSupplier(supplierID);
}
|
这个方法仅仅是调用DAL里的DiscontinueAllProductsForSupplier(supplierID)方法,并传递提供的supplierID 参数.如果有一些业务规则规定仅仅允许在一定的条件下supplier的products 才能被停止使用,那么这些规则应该写在这里(BLL).
注意:和ProductsBLL 类的UpdateProduct重载不一样,DiscontinueAllProductsForSupplier(supplierID)的签名不包括DataObjectMethodAttribute 属性(<System.ComponentModel.DataObjectMethodAttribute(System.ComponentModel.DataObjectMethodType.Update, Boolean)>).这个将DiscontinueAllProductsForSupplier(supplierID) 方法从ObjectDataSource的配置数据源向导的UPDATE标签里的下拉列表中排除.我之所以忽略这个属性是因为我们会在ASP.NET page里直接通过event handler 调用DiscontinueAllProductsForSupplier(supplierID)方法.
第五步: 为FormView添加一个“Discontinue All Products” Button
完成了BLL 和 DAL 里的DiscontinueAllProductsForSupplier(supplierID)方法后,我们来做实现停止使用选定的supplier的所有product的功能最后一步:为 FormView的 ItemTemplate添加Button .我们将这个Button 添加在supplier的phone number下,Text为“Discontinue All Products”,ID为DiscontinueAllProductsForSupplier.你可以通过FormView的智能标签里的Edit Templates 来添加这个Button (见图15),或直接修改代码.
图 15: 为FormView的ItemTemplate添加 “Discontinue All Products” Button
当用户点击这个Button 时,页面会回发,FormView的ItemCommand event被激发.我们可以为这个事件创建一个event handler ,用来在Button 被点击时执行自定义代码.注意,任何时候FormView里的任何Button, LinkButton, 或 ImageButton被点击时,ItemCommand 事件都会被激发.这意味着当用户在FormView里从一个页面跳到另一个页面时,ItemCommand 事件会被激发.当用户点击一个支持inserting, updating, 或 deleting的FormView里的New, Edit, 或 Delete 时,ItemCommand 事件会被激发.
既然无论点击什么button时, ItemCommand 都会被激发,那么在event handler里我们需要判断是“Discontinue All Products” Button 被点击了还是其它的button.为了达到这个目的,我们可以通过设置Button 的CommandName来识别. 当Button 被点击后,CommandName 的值被传到ItemCommand 的event handler,我们通过这个值来判断被点击的button是否是“Discontinue All Products” Button.设置“Discontinue All Products” Button的CommandName 为“DiscontinueProducts”.
最后我们在客户端增加一个确认框来确保用户真的想停止使用选择的supplier的所有product.和我们在为删除数据添加客户端确认 里看到的一样,这个可以用JavaScript来完成. 设置Button 的OnClientClick属性为“return confirm('This will mark _all_ of this supplier/'s products as discontinued. Are you certain you want to do this?');”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
<asp:FormView ID= "Suppliers" runat= "server" DataKeyNames= "SupplierID"
DataSourceID= "SuppliersDataSource" EnableViewState= "False"
AllowPaging= "True" >
<ItemTemplate>
<h3><asp:Label ID= "CompanyName" runat= "server"
Text= '<%# Bind("CompanyName") %>' ></asp:Label></h3>
<b>Phone:</b>
<asp:Label ID= "PhoneLabel" runat= "server" Text= '<%# Bind("Phone") %>' />
<br />
<asp:Button ID= "DiscontinueAllProductsForSupplier" runat= "server"
CommandName= "DiscontinueProducts" Text= "Discontinue All Products"
OnClientClick=" return confirm( 'This will mark _all_ of this supplier/' s
products as discontinued. Are you certain you want to do this ?');" />
</ItemTemplate>
</asp:FormView>
|
下面,为FormView的 ItemCommand 事件创建event handler . 在这个event handler 里我们需要首先判断“Discontinue All Products”Button是否被点击了.如果是,我们就需要创建一个ProductsBLL 类的实例然后调用DiscontinueAllProductsForSupplier(supplierID)方法,并将FormView里选定的SupplierID 传过去.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
protected void Suppliers_ItemCommand( object sender, FormViewCommandEventArgs e)
{
if (e.CommandName.CompareTo( "DiscontinueProducts" ) == 0)
{
// The "Discontinue All Products" Button was clicked.
// Invoke the ProductsBLL.DiscontinueAllProductsForSupplier(supplierID) method
// First, get the SupplierID selected in the FormView
int supplierID = ( int )Suppliers.SelectedValue;
// Next, create an instance of the ProductsBLL class
ProductsBLL productInfo = new ProductsBLL();
// Finally, invoke the DiscontinueAllProductsForSupplier(supplierID) method
productInfo.DiscontinueAllProductsForSupplier(supplierID);
}
}
|
注意:在FormView 里当前选定的supplier 的SupplierID 可以通过FormView的 SelectedValue property属性获取.SelectedValue 属性返回FormView里显示的记录的第一个data key的值.FormView的DataKeyNames property在绑定ObjectDataSource 到FormView 时(第二步)会自动的被设置为SupplierID .
ItemCommand event handler 创建完后,花点时间测试一下这个页面.浏览Cooperativa de Quesos 'Las Cabras' supplier (在我这是FormView 里的第五个supplier ).这个supplier 提供两种product, Queso Cabrales and Queso Manchego La Pastora,两个都没有停止使用的.
想象一下 Cooperativa de Quesos 'Las Cabras' 歇业了,因此它的产品都要被停止使用.点击“Discontinue All Products” Button.会弹出一个确认的对话框.
图 16: Cooperativa de Quesos 'Las Cabras' 供应 两种有效的产品
如果在确定对话框里点击OK,表单提交就会继续,ormView的ItemCommand 事件会被激发.然后我们创建的event handler会执行,调用DiscontinueAllProductsForSupplier(supplierID)方法,停止使用Queso Cabrales 和 Queso Manchego La Pastora这两个产品.
如果你禁用了GridView的view state,每次postback时GridView 都会重新绑定,因此这两种product被停止使用的状态马上就能显示出来(见图17).而如果没有禁用GridView的view state,你需要手动再次绑定数据.
图 17: 点击 “Discontinue All Products” Button后, Supplier的 Products被更新
第六步: 为调整Product的价格,在Business Logic Layer 创建一个UpdateProduct
和FormView里的“Discontinue All Products” Button 一样,为了在GridView 里添加提高或者降低product 的价格的button,我们首先添加Data Access Layer and Business Logic Layer 的方法.由于我们在DAL里已经有一个更新单个产品记录的方法,我们可以通过在BLL创建UpdateProduct 的重载方法来实现这个功能.
我们以前的UpdateProduct 包括一些product 字段作为输入值,我们可以为指定的product更新这些字段.我们将做少量修改,传递ProductID 和调整单价的百分比.因为不用再测定当前product 的单价,所以这样的方法会让我们在ASP.NET page 的cs文件里的代码变的更简洁.
UpdateProduct 在本指南中使用的重载方法如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public bool UpdateProduct( decimal unitPriceAdjustmentPercentage, int productID)
{
Northwind.ProductsDataTable products = Adapter.GetProductByProductID(productID);
if (products.Count == 0)
// no matching record found, return false
return false ;
Northwind.ProductsRow product = products[0];
// Adjust the UnitPrice by the specified percentage (if it's not NULL)
if (!product.IsUnitPriceNull())
product.UnitPrice *= unitPriceAdjustmentPercentage;
// Update the product record
int rowsAffected = Adapter.Update(product);
// Return true if precisely one row was updated, otherwise false
return rowsAffected == 1;
}
|
这个方法通过DAL的GetProductByProductID(productID)方法获取指定product 的信息.它会检查product的单价是否是空值.如果是,这个价格就不被更改.如果不是,product的UnitPrice 将根据指定的百分比更改(unitPriceAdjustmentPercent)
第七步: 在GridView里添加升价和降价的Button
GridView (和DetailsView)都是字段的集合.除了BoundFields, CheckBoxFields, and TemplateFields这几个字段外,ASP.NET还包含ButtonField.就象它的名字一样,ButtonField提供一个Button, LinkButton, 或 ImageButton列.和FormView一样,点击GridView 里的任何一个button— 分页,编辑或删除,排序等— 页面都会回发 ,并激发GridView的RowCommand event.
ButtonField 有一个CommandName 属性,可以用来指派特定的值给每个Button的CommandName 属性.象FormView一样,CommandName 的值用来在RowCommand event handler 里判断哪个button被点击了.
现在我们来为GridView添加两个ButtonField,一个的text为“Price +10%” ,另外一个的text为“Price -10%”. 点击GridView的智能标签里的Edit Columns link ,在左上的列表里选择ButtonField 类,点添加.
图 18: 为GridView添加两个ButtonField
移动这两个ButtonField 到GridView 的前两列.分别设置ButtonField的text为 “Price +10%” and “Price -10%”,CommandName 为“IncreasePrice” and “DecreasePrice”,.默认情况下,ButtonField 里的button为LinkButtons,这个是可通过ButtonField的ButtonType property属性来修改的.我们将这两个ButtonField设置为常规的push button.因此,设置ButtonType 属性为Button.图19显示了设置完成后的Fields 对话框的样子.而后面一个图则为GridView的页面代码.
图 19: 配置 ButtonField的 Text, CommandName, and ButtonType 属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
<asp:GridView ID= "SuppliersProducts" runat= "server" AutoGenerateColumns= "False"
DataKeyNames= "ProductID" DataSourceID= "SuppliersProductsDataSource"
EnableViewState= "False" >
<Columns>
<asp:ButtonField ButtonType= "Button" CommandName= "IncreasePrice"
Text= "Price +10%" />
<asp:ButtonField ButtonType= "Button" CommandName= "DecreasePrice"
Text= "Price -10%" />
<asp:BoundField DataField= "ProductName" HeaderText= "Product"
SortExpression= "ProductName" />
<asp:BoundField DataField= "UnitPrice" HeaderText= "Price"
SortExpression= "UnitPrice" DataFormatString= "{0:C}"
HtmlEncode= "False" />
<asp:CheckBoxField DataField= "Discontinued" HeaderText= "Discontinued"
SortExpression= "Discontinued" />
</Columns>
</asp:GridView>
|
创建ButtonField完成后,最后一步是为GridView的RowCommand 事件创建event handler .当“Price +10%”或“Price -10%”button被点击时,这个event handler需要判断被点击的那一行的ProductID ,然后调用ProductsBLL 类的UpdateProduct 方法,并将UnitPrice 的调整折扣和ProductID传进去.下来的代码会完成以上工作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
protected void SuppliersProducts_RowCommand( object sender, GridViewCommandEventArgs e)
{
if (e.CommandName.CompareTo( "IncreasePrice" ) == 0 ||
e.CommandName.CompareTo( "DecreasePrice" ) == 0)
{
// The Increase Price or Decrease Price Button has been clicked
// Determine the ID of the product whose price was adjusted
int productID =
( int )SuppliersProducts.DataKeys[Convert.ToInt32(e.CommandArgument)].Value;
// Determine how much to adjust the price
decimal percentageAdjust;
if (e.CommandName.CompareTo( "IncreasePrice" ) == 0)
percentageAdjust = 1.1M;
else
percentageAdjust = 0.9M;
// Adjust the price
ProductsBLL productInfo = new ProductsBLL();
productInfo.UpdateProduct(percentageAdjust, productID);
}
}
|
为了判断被点击“Price +10%” or “Price -10%” button 的那一行的ProductID ,我们需要用到GridView的DataKeys 集合.这个集合包含了GridView 的行的所有值.由于GridView在绑定到ObjectDataSource 时,DataKeyNames 被Visual Studio设置为ProductID ,DataKeys(rowIndex).Value 提供了指定rowIndex的ProductID .
ButtonField 会将被点击的button所在row 的rowIndex 自动传到e.CommandArgument 参数里.因此,我们用Convert.ToInt32(SuppliersProducts.DataKeys(Convert.ToInt32(e.CommandArgument)).Value)来获取被点击“Price +10%” or “Price -10%” button的行的ProductID .
和“Discontinue All Products” button里一样,如果你禁用了GridView的view state属性,每次postback时GridView 都会重新绑定.如果你这么做,你需要手动再次绑定.
图20显示当浏览Grandma Kelly's Homestead提供的product的页.图21显示当Grandma's Boysenberry Spread 的“Price +10%” button 被点击了两次和Northwoods Cranberry Sauce的“Price -10%” button被点击了一次的页面.
图20: GridView 包含“Price +10%” 和 “Price -10%” 两个Buttons
图 21: 第一和第三个产品的价格通过“Price +10%” and “Price -10%” Buttons更新
注意:GridView (和DetailsView)同样可以将Buttons,LinkButtons或ImageButtons 加到TemplateFields里.和BoundField一样,这些Button被点击时会产生回发,触发GridView的RowCommand 事件.当添加button到 TemplateField里时,button的CommandArgument 没有想使用ButtonFields一样,被自动设置为row 的index .如果你需要在RowCommand 的event handler里判断点击的button所在行的index ,你需要在TemplateField的页面代码里使用以下代码来设置button的CommandArgument 属性:
<asp:Button runat="server" ... CommandArgument='<%# CType(Container, GridViewRow).RowIndex %>' />.
总结
GridView, DetailsView, 和FormView都可以包含Buttons, LinkButtons, 或 ImageButtons.这些button被点击时,页面回发,并激发FormView 和DetailsView 的ItemCommand 事件,GridView的RowCommand 事件.这些控件都有内置的处理普通命令的功能,比如删除或编辑一条记录.然而我们一样可以使用执行自定义代码的button.
为了达到这个目的,我们需要为ItemCommand 或 RowCommand 创建一个event handler .在这个event handler 里我们首先检查CommandName 的值来判断哪个button被点击了,然后执行相应的自定义代码.在本指南里我们看到了如何使用button和ButtonField来停止使用指定supplier 的所有产品,和提高或降低特定product 的10%的价格.
祝编程快乐!
作者简介
Scott Mitchell,著有六本ASP/ASP.NET方面的书,是4GuysFromRolla.com的创始人,自1998年以来一直应用 微软Web技术。Scott是个独立的技术咨询顾问,培训师,作家,最近完成了将由Sams出版社出版的新作,24小时内精通ASP.NET 2.0。他的联系电邮为mitchell@4guysfromrolla.com,也可以通过他的博客http://scottonwriting.net/与他联系。