根據使用者限制資料修改功能 (C#)
在允許使用者編輯數據的 Web 應用程式中,不同的用戶帳戶可能會有不同的資料編輯許可權。 在本教學課程中,我們將探討如何根據瀏覽使用者動態調整數據修改功能。
簡介
許多 Web 應用程式都支援使用者帳戶,並根據登入的使用者提供不同的選項、報告和功能。 例如,透過我們的教學課程,我們可能會允許供應商公司的使用者登入網站,並更新其產品的一般資訊 - 其每個單位的名稱和數量,或許 - 以及供應商資訊,例如其公司名稱、位址、聯繫人資訊等等。 此外,我們可能會想要包含公司人員的某些用戶帳戶,讓他們可以登入並更新產品資訊,例如股票單位、重新排序層級等等。 我們的 Web 應用程式也可能允許匿名使用者造訪尚未登入) 的 (人員,但會限制他們只檢視數據。 有了這類使用者帳戶系統之後,我們想要 ASP.NET 頁面中的數據 Web 控件提供適用於目前登入使用者的插入、編輯和刪除功能。
在本教學課程中,我們將探討如何根據瀏覽使用者動態調整數據修改功能。 特別是,我們將建立一個頁面,以在可編輯的 DetailsView 中顯示供應商資訊,以及列出供應商所提供產品的 GridView。 如果瀏覽頁面的使用者來自我們的公司,他們可以:檢視任何供應商的資訊;編輯其位址;並編輯供應商提供之任何產品的資訊。 不過,如果使用者來自特定公司,他們只能檢視和編輯自己的地址資訊,而且只能編輯尚未標示為已停止的產品。
圖 1:來自我們公司的使用者可以編輯任何供應商的資訊, (按兩下即可檢視全大小的影像)
圖 2:來自特定供應商的使用者只能檢視和編輯其資訊 (按兩下即可檢視完整大小的影像)
讓我們開始吧!
注意
ASP.NET 2.0 s 成員資格系統提供標準化、可延伸的平臺,用於建立、管理和驗證用戶帳戶。 由於成員資格系統的檢查超出這些教學課程的範圍,因此本教學課程會允許匿名訪客選擇來自特定供應商或來自公司的匿名訪客,改為「假」成員資格。 如需成員資格的詳細資訊,請參閱我的 檢查 ASP.NET 2.0 s 成員資格、角色和配置檔 文章系列。
步驟 1:允許使用者指定其訪問許可權
在真實世界的 Web 應用程式中,使用者帳戶資訊會包含他們為公司或特定供應商工作,而且當使用者登入網站之後,就可以透過程式設計方式從我們的 ASP.NET 頁面存取這項資訊。 這項資訊可以透過 ASP.NET 2.0 s 角色系統擷取,如同透過配置檔系統的使用者層級帳戶資訊,或透過一些自定義方式來擷取。
由於本教學課程的目的是要示範如何根據登入的用戶來調整數據修改功能,而且不打算展示 ASP.NET 2.0 s 成員資格、角色和配置文件系統,因此我們將使用非常簡單的機制來判斷瀏覽頁面的使用者功能 - 一個 DropDownList,讓使用者可以指出他們是否應該能夠檢視和編輯任何供貨商資訊, 或者,他們可以檢視和編輯哪些特定供應商的資訊。 如果使用者指出她可以在預設) (檢視和編輯所有供應商資訊,則可以逐頁流覽所有供應商、編輯任何供應商的地址資訊,以及編輯所選供應商所提供之任何產品的名稱和數量。 如果使用者指出她只能檢視和編輯特定供應商,則她只能檢視該供應商的詳細數據和產品,而且只能針對 未 中止的產品更新每個單位資訊的名稱和數量。
在本教學課程中,我們的第一個步驟是建立此DropDownList,並將其填入系統中的供應商。 UserLevelAccess.aspx
開啟 資料夾中的頁面EditInsertDelete
,新增DropDownList,其 ID
屬性設定為 Suppliers
,並將這個DropDownList系結至名為 AllSuppliersDataSource
的新 ObjectDataSource。
圖 3:建立名為 AllSuppliersDataSource
的新 ObjectDataSource (按兩下即可檢視大小完整的影像)
由於我們想要讓DropDownList包含所有供應商,因此請將 ObjectDataSource 設定為叫 SuppliersBLL
用類別 s GetSuppliers()
方法。 此外,也請確定 ObjectDataSource s Update()
方法對應至 SuppliersBLL
類別 s UpdateSupplierAddress
方法,因為 DetailsView 也會使用此 ObjectDataSource,我們將在步驟 2 中新增。
完成 ObjectDataSource 精靈之後,設定 DropDownList 以完成步驟 Suppliers
,使其顯示 CompanyName
數據欄位,並使用 SupplierID
數據欄位作為每個 ListItem
的值。
圖 4:將 Suppliers
DropDownList 設定為 [使用 CompanyName
和 SupplierID
數據欄位] (按兩下即可檢視完整大小的影像)
此時,DropDownList 會列出資料庫中供應商的公司名稱。 不過,我們也需要在DropDownList中包含 [顯示/編輯所有供應商] 選項。 若要達成此目的,請將DropDownList s AppendDataBoundItems
屬性設定Suppliers
為 true
,然後新增ListItem
其Text
屬性為 “Show/Edit ALL Suppliers” 且其值為 -1
的 。 您可以直接透過宣告式標記或透過 Designer 加入,方法是移至 屬性視窗,然後按兩下DropDownList s Items
屬性中的省略號。
注意
如需將 Select All 專案新增至數據系結 DropDownList 的詳細討論,請參閱 使用 DropDownList 進行主要/詳細數據篩選 教學課程。
AppendDataBoundItems
設定屬性並ListItem
新增 之後,DropDownList 的宣告式標記看起來應該會像這樣:
<asp:DropDownList ID="Suppliers" runat="server" AppendDataBoundItems="True"
DataSourceID="AllSuppliersDataSource" DataTextField="CompanyName"
DataValueField="SupplierID">
<asp:ListItem Value="-1">Show/Edit ALL Suppliers</asp:ListItem>
</asp:DropDownList>
圖 5 顯示透過瀏覽器檢視時目前進度的螢幕快照。
圖 5: Suppliers
DropDownList 包含顯示全部 ListItem
,加上每個供應商的一個 (按兩下即可檢視全大小的影像)
由於我們想要在使用者變更其選取範圍之後立即更新使用者介面,請將DropDownList s AutoPostBack
屬性設定Suppliers
為 true
。 在步驟 2 中,我們將建立 DetailsView 控件,根據 DropDownList 選取項目顯示供應商 () 的資訊。 然後,在步驟 3 中,我們將為此DropDownList事件 SelectedIndexChanged
建立事件處理程式,在此事件中,我們會根據選取的供貨商,將適當的供貨商資訊系結至 DetailsView 的程式代碼。
步驟 2:新增 DetailsView 控件
讓我們使用DetailsView來顯示供貨商資訊。 對於可檢視和編輯所有供應商的使用者,DetailsView 將支援分頁,讓使用者逐一查看供應商資訊一次一筆記錄。 不過,如果使用者適用於特定供應商,DetailsView 只會顯示該特定供應商的資訊,而且不會包含分頁介面。 不論是哪一種情況,DetailsView 都必須允許使用者編輯供應商的位址、城市和國家/地區字段。
將 DetailsView 新增至 DropDownList 下方 Suppliers
的頁面,將其 ID
屬性設定為 SupplierDetails
,並將它系結至 AllSuppliersDataSource
上一個步驟中建立的 ObjectDataSource。 接下來,從 DetailsView 智慧標記中選取 [啟用分頁] 和 [啟用編輯] 複選框。
注意
如果您在 DetailsView 智慧標記中看不到 [啟用編輯] 選項,因為您未將 ObjectDataSource s Update()
方法對應至 SuppliersBLL
類別 s UpdateSupplierAddress
方法。 請花點時間返回並進行此設定變更,之後[啟用編輯] 選項應該會出現在DetailsView智慧標記中。
由於 類別 SuppliersBLL
s UpdateSupplierAddress
方法只接受四個參數 - supplierID
、 address
city
和 country
- 修改 DetailsView s BoundFields,CompanyName
讓 和 Phone
BoundFields 是唯讀的。 此外,請完全移除 SupplierID
BoundField。 最後, AllSuppliersDataSource
ObjectDataSource 的 屬性目前 OldValuesParameterFormatString
設定為 original_{0}
。 請花點時間從宣告式語法中移除這個屬性設定,或將其設定為預設值 {0}
。
設定 SupplierDetails
DetailsView 和 AllSuppliersDataSource
ObjectDataSource 之後,我們將具有下列宣告式標記:
<asp:ObjectDataSource ID="AllSuppliersDataSource" runat="server"
SelectMethod="GetSuppliers" TypeName="SuppliersBLL"
UpdateMethod="UpdateSupplierAddress">
<UpdateParameters>
<asp:Parameter Name="supplierID" Type="Int32" />
<asp:Parameter Name="address" Type="String" />
<asp:Parameter Name="city" Type="String" />
<asp:Parameter Name="country" Type="String" />
</UpdateParameters>
</asp:ObjectDataSource>
<asp:DetailsView ID="SupplierDetails" runat="server" AllowPaging="True"
AutoGenerateRows="False" DataKeyNames="SupplierID"
DataSourceID="AllSuppliersDataSource">
<Fields>
<asp:BoundField DataField="CompanyName" HeaderText="Company"
ReadOnly="True" SortExpression="CompanyName" />
<asp:BoundField DataField="Address" HeaderText="Address"
SortExpression="Address" />
<asp:BoundField DataField="City" HeaderText="City"
SortExpression="City" />
<asp:BoundField DataField="Country" HeaderText="Country"
SortExpression="Country" />
<asp:BoundField DataField="Phone" HeaderText="Phone" ReadOnly="True"
SortExpression="Phone" />
<asp:CommandField ShowEditButton="True" />
</Fields>
</asp:DetailsView>
此時,詳細數據檢視可以逐頁查看,而且不論在DropDownList中 Suppliers
所做的選取項目為何,都可以更新選取的供應商地址資訊 (請參閱圖 6) 。
圖 6:可檢視任何供應商資訊,並在單擊以 檢視大小完整的影像 (更新其位址)
步驟 3:只顯示選取的供應商資訊
我們頁面目前會顯示所有供應商的資訊,不論特定供應商是否已從 Suppliers
DropDownList 中選取。 為了只顯示所選供應商的供應商的供應商資訊,我們需要將另一個 ObjectDataSource 新增至我們的頁面,其中一個擷取特定供應商的相關信息。
將新的 ObjectDataSource 新增至頁面,並將其命名為 SingleSupplierDataSource
。 從智慧標記中,按兩下 [設定資料源] 連結,並讓它使用 SuppliersBLL
類別 s GetSupplierBySupplierID(supplierID)
方法。 AllSuppliersDataSource
如同 ObjectDataSource,將 SingleSupplierDataSource
ObjectDataSource s Update()
方法對應至 SuppliersBLL
類別 s UpdateSupplierAddress
方法。
圖 7:將 SingleSupplierDataSource
ObjectDataSource 設定為使用 GetSupplierBySupplierID(supplierID)
方法 (按兩下即可檢視完整大小的影像)
接下來,我們會提示您指定方法輸入參數的參數supplierID
來源GetSupplierBySupplierID(supplierID)
。 由於我們想要顯示從DropDownList選取之供貨商的資訊,因此請使用 Suppliers
DropDownList s SelectedValue
屬性作為參數來源。
圖 8:使用 Suppliers
DropDownList 作為 supplierID
參數來源 (按兩下即可檢視完整大小的影像)
即使新增了第二個 ObjectDataSource,DetailsView 控件目前仍設定為一律使用 AllSuppliersDataSource
ObjectDataSource。 我們需要新增邏輯,根據選取的DropDownList項目來調整DetailsView Suppliers
所使用的數據源。 若要達成此目的,請建立 SelectedIndexChanged
供應商 DropDownList 的事件處理程式。 按兩下 Designer中的DropDownList,即可更輕鬆地建立此動作。 此事件處理程式必須判斷要使用的數據源,而且必須將數據重新系結至 DetailsView。 這是使用下列程式代碼完成的:
protected void Suppliers_SelectedIndexChanged(object sender, EventArgs e)
{
if (Suppliers.SelectedValue == "-1")
{
// The "Show/Edit ALL" option has been selected
SupplierDetails.DataSourceID = "AllSuppliersDataSource";
// Reset the page index to show the first record
SupplierDetails.PageIndex = 0;
}
else
// The user picked a particular supplier
SupplierDetails.DataSourceID = "SingleSupplierDataSource";
// Ensure that the DetailsView is in read-only mode
SupplierDetails.ChangeMode(DetailsViewMode.ReadOnly);
// Need to "refresh" the DetailsView
SupplierDetails.DataBind();
}
事件處理程式會從判斷是否已選取 [顯示/編輯所有供應商] 選項開始。 如果是,它會將 DetailsView 設定SupplierDetails
DataSourceID
為 AllSuppliersDataSource
,並將屬性設定PageIndex
為 0,將使用者傳回供應商集合中的第一筆記錄。 不過,如果使用者已從DropDownList 選取特定供應商,則會將DetailsView指派 DataSourceID
給 SingleSuppliersDataSource
。 不論使用何種數據源, SuppliersDetails
模式都會還原回只讀模式,而數據會透過呼叫 SuppliersDetails
控件的 DataBind()
方法重新系結至 DetailsView。
有了這個事件處理程式,DetailsView 控件現在會顯示選取的供應商,除非已選取 [顯示/編輯所有供應商] 選項,在此情況下,所有供應商都可以透過分頁介面來檢視。 圖 9 顯示已選取 [顯示/編輯所有供應商] 選項的頁面;請注意,分頁介面存在,可讓用戶造訪並更新任何供應商。 圖 10 顯示已選取 Ma 版供應商的頁面。 在此案例中,只有Ma Powers資訊可供檢視和編輯。
圖 9:您可以檢視和編輯所有供應商資訊, (按兩下即可檢視全大小的影像)
圖 10:按兩下即可檢視和編輯選取的供應商資訊 (按鍵即可檢視完整大小的影像)
注意
在本教學課程中,DropDownList 和 DetailsView 控件 EnableViewState
都必須設定為 true
(預設) ,因為 DropDownList s SelectedIndex
和 DetailsView DataSourceID
屬性的變更必須在回傳之間記住。
步驟 4:在可編輯的 GridView 中列出供應商產品
完成 DetailsView 之後,下一個步驟是包含可編輯的 GridView,其中列出所選供應商所提供的這些產品。 此 GridView 應該只 ProductName
允許編輯 和 QuantityPerUnit
欄位。 此外,如果流覽頁面的使用者來自特定供應商,就應該只允許更新 未 中止的產品。 為了達成此目的,我們必須先新增類別 方法的多ProductsBLL
載,以只ProductID
接受、 ProductName
和 QuantityPerUnit
字段做為UpdateProducts
輸入。 我們已在許多教學課程中事先逐步完成此程式,因此讓我們在這裡查看程序代碼,這應該新增至 ProductsBLL
:
[System.ComponentModel.DataObjectMethodAttribute(
System.ComponentModel.DataObjectMethodType.Update, false)]
public bool UpdateProduct(string productName, string quantityPerUnit, 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];
product.ProductName = productName;
if (quantityPerUnit == null)
product.SetQuantityPerUnitNull();
else
product.QuantityPerUnit = quantityPerUnit;
// Update the product record
int rowsAffected = Adapter.Update(product);
// Return true if precisely one row was updated, otherwise false
return rowsAffected == 1;
}
建立此多載之後,我們即可新增 GridView 控件及其相關聯的 ObjectDataSource。 將新的 GridView 新增至頁面、將其 ID
屬性設定為 ProductsBySupplier
,並將它設定為使用名為 ProductsBySupplierDataSource
的新 ObjectDataSource。 由於我們想要讓 GridView 依選取的供應商列出這些產品,請使用 ProductsBLL
類別 s GetProductsBySupplierID(supplierID)
方法。 此外,將 Update()
方法對應至我們剛才建立的新 UpdateProduct
多載。
圖 11:將 ObjectDataSource 設定為使用 UpdateProduct
[剛建立的多載] (按兩下即可檢視完整大小的映像)
我們提示您選取方法輸入參數的參數supplierID
來源GetProductsBySupplierID(supplierID)
。 因為我們想要顯示 DetailsView 中所選取供應商的產品,所以請使用 SuppliersDetails
DetailsView 控件的 SelectedValue
屬性做為參數來源。
圖 12:使用 SuppliersDetails
DetailsView s SelectedValue
屬性作為參數來源 (按兩下即可檢視大小完整的影像)
返回 GridView,移除 、 QuantityPerUnit
和 Discontinued
以外的ProductName
所有 GridView 字段,將 CheckBoxField 標示Discontinued
為唯讀。 此外,從 GridView 智慧標記中檢查 [啟用編輯] 選項。 在進行這些變更之後,GridView 和 ObjectDataSource 的宣告式標記看起來應該如下所示:
<asp:GridView ID="ProductsBySupplier" runat="server" AutoGenerateColumns="False"
DataKeyNames="ProductID" DataSourceID="ProductsBySupplierDataSource">
<Columns>
<asp:CommandField ShowEditButton="True" />
<asp:BoundField DataField="ProductName" HeaderText="Product"
SortExpression="ProductName" />
<asp:BoundField DataField="QuantityPerUnit" HeaderText="Qty/Unit"
SortExpression="QuantityPerUnit" />
<asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued"
ReadOnly="True" SortExpression="Discontinued" />
</Columns>
</asp:GridView>
<asp:ObjectDataSource ID="ProductsBySupplierDataSource" runat="server"
OldValuesParameterFormatString="original_{0}" TypeName="ProductsBLL"
SelectMethod="GetProductsBySupplierID" UpdateMethod="UpdateProduct">
<UpdateParameters>
<asp:Parameter Name="productName" Type="String" />
<asp:Parameter Name="quantityPerUnit" Type="String" />
<asp:Parameter Name="productID" Type="Int32" />
</UpdateParameters>
<SelectParameters>
<asp:ControlParameter ControlID="SupplierDetails" Name="supplierID"
PropertyName="SelectedValue" Type="Int32" />
</SelectParameters>
</asp:ObjectDataSource>
如同先前的 ObjectDataSources,這個 OldValuesParameterFormatString
屬性設定 original_{0}
為 ,這會在嘗試更新每個單位的產品名稱或數量時造成問題。 請從宣告式語法移除這個屬性,或將其設定為預設值 {0}
。
完成此設定后,我們的頁面現在會列出 GridView 中所選取供應商所提供的產品, (請參閱圖 13) 。 目前 可以更新每個單位的任何 產品名稱或數量。 不過,我們需要更新頁面邏輯,以便針對與特定供應商相關聯的使用者停止使用這類功能。 我們將在步驟 5 中處理這最後一個部分。
圖 13:選取供應商所提供的產品會顯示 (按兩下即可檢視全大小的影像)
注意
新增這個可編輯的 GridView 時, Suppliers
應該更新 DropDownList 的 SelectedIndexChanged
事件處理程式,以將 GridView 傳回唯讀狀態。 否則,如果在編輯產品資訊中間選取不同的供應商,則新供應商的 GridView 中對應的索引也可以編輯。 若要避免這種情況,只要在事件處理程式中SelectedIndexChanged
將 GridView 的 EditIndex
屬性設定為 -1
。
此外,請記得,在預設行為) (啟用 GridView 檢視狀態非常重要。 如果您將 GridView 的 EnableViewState
屬性設定為 false
,則會執行不小心刪除或編輯記錄並行用戶的風險。
步驟 5:未選取 [顯示/編輯所有供應商] 時,不允許編輯已停止產品的編輯
ProductsBySupplier
雖然 GridView 功能完全正常,但目前會授與來自特定供應商的使用者太多存取權。 根據我們的商務規則,這類使用者應該無法更新已停止的產品。 若要強制執行此動作,我們可以隱藏 (或停用) 當供應商的使用者瀏覽頁面時,這些 GridView 數據列中已停止產品的 [編輯] 按鈕。
建立 GridView 事件的 RowDataBound
事件處理程式。 在此事件處理程式中,我們需要判斷使用者是否與特定供應商相關聯,在本教學課程中,可以藉由檢查供應商 DropDownList s SelectedValue
屬性來判斷 -1 以外的專案,則使用者會與特定供應商相關聯。 對於這類使用者,我們接著需要判斷產品是否已停止。 我們可以透過 e.Row.DataItem
屬性擷取系結至 GridView 數據列之實際ProductRow
實例的參考,如在 GridView 頁尾中顯示摘要資訊教學課程中所討論。 如果產品已停止,我們可以使用上一個教學課程中討論的技術,擷取 GridView s CommandField 中 [編輯] 按鈕的程式設計參考: 在刪除時新增 Client-Side 確認。 一旦有參考,就可以隱藏或停用按鈕。
protected void ProductsBySupplier_RowDataBound(object sender, GridViewRowEventArgs e)
{
if (e.Row.RowType == DataControlRowType.DataRow)
{
// Is this a supplier-specific user?
if (Suppliers.SelectedValue != "-1")
{
// Get a reference to the ProductRow
Northwind.ProductsRow product =
(Northwind.ProductsRow)((System.Data.DataRowView)e.Row.DataItem).Row;
// Is this product discontinued?
if (product.Discontinued)
{
// Get a reference to the Edit LinkButton
LinkButton editButton = (LinkButton)e.Row.Cells[0].Controls[0];
// Hide the Edit button
editButton.Visible = false;
}
}
}
}
有了這個事件處理程式,當以特定供應商的使用者身分瀏覽此頁面時,已停止的產品將無法編輯,因為這些產品會隱藏 [編輯] 按鈕。 例如,Chef Anton s Reviewsbo Mix 是 New 一家新工廠的 Cajun 點感供應商已中止的產品。 流覽此特定供應商的頁面時,此產品的 [編輯] 按鈕會隱藏在看見 (請參閱圖 14) 。 不過,使用 「顯示/編輯所有供應商」瀏覽時,可以使用 [編輯] 按鈕 (請參閱圖 15) 。
圖 14:對於 Supplier-Specific 使用者而言,Chef Anton s 的 [編輯] 按鈕是隱藏的 (按兩下即可檢視完整大小的影像)
圖 15:針對 [顯示/編輯所有供貨商使用者],[Chef Antons混合] 的 [編輯] 按鈕會顯示 (按兩下即可檢視完整大小的影像)
檢查商業規則層中的訪問許可權
在本教學課程中,ASP.NET 頁面會處理使用者可以看到哪些資訊及其可更新之產品的所有邏輯。 在理想情況下,此邏輯也會出現在商業規則層。 例如, SuppliersBLL
類別 s GetSuppliers()
方法 (傳回所有供應商) 可能包含檢查,以確保目前登入的使用者 未 與特定供應商相關聯。 同樣地, UpdateSupplierAddress
方法可以包含檢查,以確保目前登入的使用者為公司 (工作,因此可以更新所有供應商地址資訊) 或與正在更新數據的供應商相關聯。
我在這裡未包含這類 BLL 層檢查,因為在教學課程中,用戶權力是由頁面上的DropDownList所決定,BLL 類別無法存取。 使用成員資格系統或 ASP.NET (提供的现成验证配置之一,例如 Windows 驗證) 時,可以從 BLL 存取目前登入的使用者資訊和角色資訊,進而讓簡報和 BLL 層都能進行這類訪問許可權檢查。
摘要
大部分提供用戶帳戶的網站都需要根據登入的使用者自定義數據修改介面。 系統管理使用者可以刪除和編輯任何記錄,而非系統管理使用者可能僅限於更新或刪除自己建立的記錄。 無論案例為何,都可以擴充數據 Web 控件、ObjectDataSource 和 Business Logic Layer 類別,根據登入的使用者新增或拒絕特定功能。 在本教學課程中,我們瞭解如何根據使用者是否與特定供應商相關聯,或是否為公司工作,來限制可檢視和可編輯的數據。
本教學課程最後會使用 GridView、DetailsView 和 FormView 控件來檢查插入、更新和刪除數據。 從下一個教學課程開始,我們將注意新增分頁和排序支援。
快樂的程序設計!
關於作者
Scott Mitchell 是 1998 年以來,1998 年與 Microsoft Web 技術合作的 七篇 ASP/ASP.NET 書籍和 4GuysFromRolla.com 作者。 Scott 是獨立的顧問、訓練者和作者。 他的最新書籍是 Sams 在 24 小時內自行 ASP.NET 2.0。 您可以透過mitchell@4GuysFromRolla.com部落格連到,也可以透過其部落格來存取,網址為 http://ScottOnWriting.NET。