一种创新的.NET业务Web Forms开发架构(1)-概述
本系列文章介绍了一种用于在企业软件开发中构建业务Web Forms的创新架构,与传统的ASP.NET或MVC开发相比,它具有更好的性能、更高的生产力、更强的可配置性和更易于维护性。
引言
本系列文章介绍了一种用于在企业软件开发中构建业务Web Forms的创新架构,与传统的ASP.NET或MVC开发相比,它具有更好的性能、更高的生产力、更强的可配置性和更易于维护性。这是第一篇文章,介绍了传统ASP.NET和MVC开发所面临的问题以及改进这些问题的新解决方案。然后,我将提供一个关于在新解决方案中开发产品管理Web Form的代码片段。
问题
当我们在ASP.NET或MVC中开发Web Form时,我们需要处理以下事项:
- 创建视图模板和代码隐藏(控制器)
- 样式调整
- 为所有操作编写事件处理程序
- 手动控制Web控件在不同操作下的可见性
- 手动集成权限
- 难以编写单元测试(MVC除外)
- 其他
有了这些内容,开发的Web Forms的源代码通常非常复杂,混合了Web控件可见性控制、业务逻辑、样式管理、权限检查等逻辑。Web Forms的生产力较低,容易出错且难以维护。
新解决方案
RapidWebDev解决方案实现了一个基于ASP.NET的新UI框架,旨在解决面临的问题。其设计原则是抽象出常规业务场景下Web Forms的需求,并定义XML模式来配置Web Forms,从而使用户行为、样式和权限都由框架管理。框架的某些接口需要实现,用于回调用户行为。例如,有一个带有过滤器配置的查询面板。当用户单击查询面板中的查询按钮时,将调用实现IDynamicPage
接口的Query
方法,并传入查询参数。
通过这种设计,
- Web Forms可配置且易于维护
- 代码复杂度明显降低
- 实现可重用且可测试
- 通过框架统一UI样式
因此,本文介绍RapidWebDev
UI框架的路线图是介绍:
- Web Form结构/布局
- Web Form组件之间的通信
- 开发Web Form所需条件
- 一个产品管理示例应用程序
- 权限如何工作
本文不介绍架构的详细实现,而是侧重于高层次的创新点。我将在后续文章中详细介绍。
Web Form结构
我们为常规业务场景抽象出通用的面板类型 - “查询面板”用于设置查询过滤器;“网格面板”用于显示查询到的记录;“详细信息面板”用于创建/更新/查看/删除单个记录;“按钮面板”用于配置自定义操作按钮;“聚合面板”用于批量处理网格中选定的多个记录等。
让我们通过一些企业软件中的常见业务场景。
- 在产品管理Web Form中,一些用户可以创建产品草稿、审批产品、导出或转发产品。应该有一个“查询面板”供用户设置过滤器,一个“网格面板”以列表形式显示查询到的产品,一个“详细信息面板”用于创建/更新/查看单个产品的详细信息,一个审批面板(“聚合面板”)用于审批网格中选定的产品,以及一个转发面板(“聚合面板”)用于通过邮件将选定的产品转发给其他人等。
- 在入库产品Web Form中,用户可以在扫描面板(带有产品编号输入框的“详细信息面板”)中扫描产品编号,并在临时“网格面板”中显示扫描到的产品。用户可以提交网格中选定的多个扫描产品,并获得一个入库单(“聚合面板”)。
- 在新闻管理Web Form中,用户可以在“查询面板”中设置新的查询过滤器,并在“网格面板”中显示它们。用户可以在“详细信息面板”中添加新新闻,或选择现有新闻进行更新、删除或查看详细信息。
让我们通过以下截图预览这些面板类型。
查询、按钮和网格面板

详细信息面板

聚合面板

Web Form面板之间的通信
面板之间的通信通过以下流程图描述:

查询面板
当用户单击查询面板中的*查询*按钮时,查询面板会收集所有查询过滤器,并向Web服务器发送异步请求,然后将记录拉取并渲染到网格面板。
网格面板
显示查询到的记录。网格中的每行都可以配置三个按钮:编辑、查看和删除。当用户单击行中的编辑/查看按钮时,将显示详细信息面板。在编辑模式下,用户成功保存编辑记录后,网格会自动刷新。网格支持列大小调整、显示/隐藏列、排序、分页、自动行预览。
按钮面板
在Web Form中配置自定义按钮,每个按钮都分配了一个命令参数。单击按钮时,将显示具有相同命令参数的聚合面板。但有三个预定义的命令参数不与聚合面板相关。它们是Add
、Print
和DownloadToExcel
。单击Add
按钮时会显示一个空的详细信息面板。Print
和DownloadToExcel
按钮用于打印或将所有网格记录(包括其他分页)导出到Excel,而无需编写任何代码。
详细信息面板
创建/更新/查看单个记录。用户在详细信息面板中保存后,网格面板会自动刷新。
聚合面板
任何自定义操作。用户在聚合面板中保存后,网格面板会自动刷新。
开发Web Form所需条件
Web Form需要XML配置和实现IDynamicPage
接口。在XML配置中,我们应配置查询过滤器、网格字段、按钮、详细信息面板和聚合面板。不要担心静态XML配置中的动态数据。XML配置允许配置回调处理器。
当Web Form中需要详细信息面板时,我们需要开发一个不带代码隐藏的ascx模板,并实现IDetailPanel
接口。ascx模板用于渲染详细信息面板中的Web控件,如下截图所示。IDetailPanel
实现将ascx模板集成到业务逻辑中。
聚合面板的开发与详细信息面板相同。
一个产品管理示例应用程序
我们有一个产品管理Web Form的要求如下:
- 创建新产品
- 编辑/查看现有产品
- 删除单个产品
- 批量删除多个选定的产品
- 打印查询到的产品
- 将查询到的产品导出到Excel
- 编辑/查看产品时显示产品更改日志
步骤 1
实现用于查询产品和删除单个产品的动态页面接口。您可以看到,在Query方法中,我们实际上并没有组合查询表达式。框架传递的“parameter”参数可以直接转换为LinqPredicate。框架不仅使用查询面板XML配置来渲染UI,还组合查询表达式。因此,当我们需要更改查询过滤器时,只需更改XML配置而无需编译。
/// <summary>
/// Dynamic page to manage products
/// </summary>
public class ProductDynamicPage : DynamicPage
{
private IAuthenticationContext authenticationContext =
SpringContext.Current.GetObject<IAuthenticationContext>();
/// <summary>
/// Query products by parameters.
/// </summary>
/// <param name="parameter"></param>
/// <returns></returns>
public override QueryResults Query(QueryParameter parameter)
{
using (ProductManagementDataContext ctx =
DataContextFactory.Create<ProductManagementDataContext>())
{
IQueryable<Product> q = from p in ctx.Products
where p.ApplicationId ==
authenticationContext.ApplicationId
select p;
LinqPredicate predicate = parameter.Expressions.Compile();
if (predicate != null && !string.IsNullOrEmpty(predicate.Expression))
q = q.Where(predicate.Expression, predicate.Parameters);
if (parameter.SortExpression != null)
q = q.OrderBy(parameter.SortExpression.Compile());
int recordCount = q.Count();
var results = q.Skip(parameter.PageIndex *
parameter.PageSize).Take(parameter.PageSize).ToList();
return new QueryResults(recordCount, results);
}
}
/// <summary>
/// Delete the product by id.
/// </summary>
/// <param name="entityId"></param>
public override void Delete(string entityId)
{
Guid productId = new Guid(entityId);
using (TransactionScope transactionScope = new TransactionScope())
using (ProductManagementDataContext ctx =
DataContextFactory.Create<ProductManagementDataContext>())
{
ctx.ProductLogs.Delete(log => log.ProductId == productId);
ctx.Products.Delete(p => p.Id == productId);
ctx.SubmitChanges();
transactionScope.Complete();
}
}
}
第二步
为产品详细信息面板创建一个ascx模板。该模板用于渲染添加/更新/查看产品的表单。ASP.NET服务器控件将通过ID自动绑定到详细信息面板实现类的成员声明。
<ajax:TabContainer ID="TabContainer" runat="server">
<ajax:TabPanel ID="TabPanelProduct" HeaderText="Product" runat="server">
<ContentTemplate>
<table cellpadding="0" cellspacing="0" class="table6col">
<tr>
<td class="c1" nowrap="nowrap">Category: </td>
<td class="c2">
<My:ComboBox ID="DropDownListCategory" Mode="Local"
Editable="true" ForceSelection="true" runat="server" />
<label for="<%= this.DropDownListCategory.ClientID %>"
class="required">*</label>
</td>
<td class="c1" nowrap="nowrap">Name: </td>
<td class="c2">
<My:TextBox ID="TextBoxName" CssClass="textboxShort"
MaxLength="256" runat="server" />
<label for="<%= this.TextBoxName.ClientID %>"
class="required">*</label>
</td>
<td class="c1" nowrap="nowrap">Number: </td>
<td class="c2">
<My:TextBox ID="TextBoxNumber"
CssClass="textboxShort" MaxLength="32" runat="server" />
<label for="<%= this.TextBoxNumber.ClientID %>"
class="required">*</label>
</td>
</tr>
<tr>
<td class="c1" nowrap="nowrap">Manufactory: </td>
<td class="span" colspan="5">
<My:TextBox ID="TextBoxManufactory" CssClass="textarea"
MaxLength="256" Width="92.7%" runat="server" />
</td>
</tr>
<tr>
<td class="c1" nowrap="nowrap">Description: </td>
<td class="span" colspan="5">
<My:TextBox ID="TextBoxDescription" CssClass="textarea"
Width="92.7%" runat="server" />
</td>
</tr>
<My:ExtensionDataForm ID="ProductExtensionDataForm" runat="server" />
<asp:PlaceHolder ID="PlaceHolderOperateContext" Visible="false"
runat="server">
<tr>
<td colspan="6"><hr /></td>
</tr>
<tr>
<td class="c1" nowrap="true">Created On: </td>
<td class="c2">
<My:TextBox ID="TextBoxCreatedOn"
CssClass="textboxShort readonly"
ReadOnly="true" runat="server" />
</td>
<td class="c1" nowrap="true">Created By: </td>
<td class="span" colspan="3">
<My:UserLink ID="UserLinkCreatedBy" runat="server" />
</td>
</tr>
<tr>
<td class="c1" nowrap="true">Updated On: </td>
<td class="c2">
<My:TextBox ID="TextBoxLastUpdatedOn"
CssClass="textboxShort readonly" ReadOnly="true"
runat="server" />
</td>
<td class="c1" nowrap="true">Updated By: </td>
<td class="span" colspan="3">
<My:UserLink ID="UserLinkLastUpdatedBy" runat="server" />
</td>
</tr>
</tr>
</asp:PlaceHolder>
</table>
</ContentTemplate>
</ajax:TabPanel>
<ajax:TabPanel ID="TabPanelProductLogs" HeaderText="Logs" Visible="false"
runat="server">
<ContentTemplate>
<table cellpadding="2" cellspacing="0" border="1" style="width:100%;
border-collapse:separate">
<asp:Repeater ID="RepeaterProductLogs" runat="server">
<HeaderTemplate>
<tr>
<th style="width: 60px; padding:2px;
background-color: Gray; color: White">
Number
</th>
<th style="padding:2px; background-color: Gray;
color: White">Body</th>
<th style="width: 120px; padding:2px;
background-color: Gray; color: White">
User
</th>
<th style="width: 170px; padding:2px;
background-color: Gray; color: White">
Logged On
</th>
</tr>
</HeaderTemplate>
<ItemTemplate>
<tr>
<td style="text-align:center; padding:2px">
<%# Container.ItemIndex + 1 %>
</td>
<td style="padding:2px">
<%# DataBinder.Eval(Container.DataItem, "Body") %>
</td>
<td style="padding:2px">
<%# UserLink.BuildUserLink(
DataBinder.Eval(Container.DataItem,
"LoggedBy").ToString())%>
</td>
<td style="padding:2px">
<%# DataBinder.Eval(Container.DataItem, "LoggedOn") %>
</td>
</tr>
</ItemTemplate>
</asp:Repeater>
</table>
</ContentTemplate>
</ajax:TabPanel>
</ajax:TabContainer>
步骤 3
实现用于添加/更新/查看单个产品的详细信息面板接口。框架会自动从ascx模板解析具有自定义属性*Binding*的数据成员的引用。数据成员名应与模板中的服务器控件ID相同。*Binding(string parentControlPath)*的参数*parentControlPath*表示包含该控件的ASP.NET*ITemplate*控件。
/// <summary>
/// Detail panel page for products.
/// </summary>
public class ProductDetailPanel : DetailPanelPage
{
#region Binding Controls
[Binding("TabContainer/TabPanelProduct")]
protected DropDownList DropDownListCategory;
[Binding("TabContainer/TabPanelProduct")]
protected TextBox TextBoxName;
[Binding("TabContainer/TabPanelProduct")]
protected TextBox TextBoxNumber;
[Binding("TabContainer/TabPanelProduct")]
protected TextBox TextBoxManufactory;
[Binding("TabContainer/TabPanelProduct")]
protected TextBox TextBoxDescription;
[Binding("TabContainer/TabPanelProduct")]
protected ExtensionDataForm ProductExtensionDataForm;
[Binding("TabContainer")]
protected AjaxControlToolkit.TabPanel TabPanelProductLogs;
[Binding("TabContainer/TabPanelProduct")]
protected PlaceHolder PlaceHolderOperateContext;
[Binding("TabContainer/TabPanelProduct")]
protected TextBox TextBoxCreatedOn;
[Binding("TabContainer/TabPanelProduct")]
protected UserLink UserLinkCreatedBy;
[Binding("TabContainer/TabPanelProduct")]
protected TextBox TextBoxLastUpdatedOn;
[Binding("TabContainer/TabPanelProduct")]
protected UserLink UserLinkLastUpdatedBy;
[Binding("TabContainer/TabPanelProductLogs")]
protected Repeater RepeaterProductLogs;
#endregion
private IConcreteDataApi concreteDataApi =
SpringContext.Current.GetObject<IConcreteDataApi>();
private IMetadataApi metadataApi = SpringContext.Current.GetObject<IMetadataApi>();
private IAuthenticationContext authenticationContext =
SpringContext.Current.GetObject<IAuthenticationContext>();
/// <summary>
/// Load product categories into dropdown control when the form is loaded.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public override void OnLoad(IRequestHandler sender, DetailPanelPageEventArgs e)
{
if (!sender.IsPostBack)
{
var productCategories = concreteDataApi.FindAllByType("ProductCategory")
.Where(c => c.DeleteStatus == DeleteStatus.NotDeleted)
.OrderBy(c => c.Name);
this.DropDownListCategory.Items.Clear();
this.DropDownListCategory.Items.Add("");
this.DropDownListCategory.SelectedIndex = 0;
foreach (ConcreteDataObject productCategory in productCategories)
this.DropDownListCategory.Items.Add
(new ListItem(productCategory.Name, productCategory.Id.ToString()));
}
IObjectMetadata productMetadata = metadataApi.GetType("Product");
if (ProductExtensionDataForm != null)
this.ProductExtensionDataForm.CreateDataInputForm(productMetadata.Id);
}
/// <summary>
/// Load an existed product into UI.
/// </summary>
/// <param name="entityId"></param>
public override void LoadWritableEntity(string entityId)
{
Guid productId = new Guid(entityId);
using (ProductManagementDataContext ctx =
DataContextFactory.Create<ProductManagementDataContext>())
{
Product p = ctx.Products.FirstOrDefault(product =>
product.Id == productId && product.ApplicationId ==
authenticationContext.ApplicationId);
if (p == null)
throw new ValidationException("The product id doesn't exist.");
// load hardwired properties.
this.TextBoxName.Text = p.Name;
this.TextBoxNumber.Text = p.Number;
this.DropDownListCategory.SelectedValue = p.CategoryId.ToString();
this.TextBoxDescription.Text = p.Description;
this.TextBoxManufactory.Text = p.Manufactory;
// load dynamic properties.
this.ProductExtensionDataForm.SetControlValuesFromObjectProperties(p);
this.PlaceHolderOperateContext.Visible = true;
this.UserLinkCreatedBy.UserId = p.CreatedBy.ToString();
this.TextBoxCreatedOn.Text =
LocalizedDateTime.ToDateTimeString(p.CreatedOn);
if (p.LastUpdatedBy.HasValue)
this.UserLinkLastUpdatedBy.UserId = p.LastUpdatedBy.ToString();
if (p.LastUpdatedOn.HasValue)
this.TextBoxLastUpdatedOn.Text =
LocalizedDateTime.ToDateTimeString(p.LastUpdatedOn.Value);
// bind the product logs.
this.TabPanelProductLogs.Visible = true;
this.RepeaterProductLogs.DataSource =
p.ProductLogs.OrderBy(l => l.LoggedOn).ToList();
this.RepeaterProductLogs.DataBind();
}
}
/// <summary>
/// Create a new product.
/// </summary>
/// <returns></returns>
public override string Create()
{
using (ProductManagementDataContext ctx =
DataContextFactory.Create<ProductManagementDataContext>())
{
this.Validate(ctx, null);
IObjectMetadata productMetadata = metadataApi.GetType("Product");
Product p = new Product
{
Name = this.TextBoxName.Text,
Number = this.TextBoxNumber.Text,
CategoryId = new Guid(this.DropDownListCategory.SelectedValue),
ApplicationId = authenticationContext.ApplicationId,
Description = this.TextBoxDescription.Text,
Manufactory = this.TextBoxManufactory.Text,
CreatedBy = authenticationContext.User.UserId,
CreatedOn = DateTime.Now,
ExtensionDataTypeId = productMetadata != null ?
productMetadata.Id : Guid.Empty
};
if (ProductExtensionDataForm != null)
this.ProductExtensionDataForm.SetObjectPropertiesFromControlValues(p);
ctx.Products.InsertOnSubmit(p);
ProductLog productLog = new ProductLog
{
Product = p,
Body = "The product is created",
LoggedBy = authenticationContext.User.UserId,
LoggedOn = DateTime.Now
};
ctx.ProductLogs.InsertOnSubmit(productLog);
ctx.SubmitChanges();
return p.Id.ToString();
}
}
/// <summary>
/// Update an existed product.
/// </summary>
/// <param name="entityId"></param>
public override void Update(string entityId)
{
Guid productId = new Guid(entityId);
using (ProductManagementDataContext ctx =
DataContextFactory.Create<ProductManagementDataContext>())
{
this.Validate(ctx, productId);
Product p = ctx.Products.FirstOrDefault(product => product.Id == productId);
if (p == null)
throw new ValidationException("The product id doesn't exist.");
p.Name = this.TextBoxName.Text;
p.Number = this.TextBoxNumber.Text;
p.CategoryId = new Guid(this.DropDownListCategory.SelectedValue);
p.Description = this.TextBoxDescription.Text;
p.Manufactory = this.TextBoxManufactory.Text;
p.LastUpdatedBy = authenticationContext.User.UserId;
p.LastUpdatedOn = DateTime.Now;
if (this.ProductExtensionDataForm != null)
this.ProductExtensionDataForm.SetObjectPropertiesFromControlValues(p);
ProductLog productLog = new ProductLog
{
Product = p,
Body = "The product is modified...",
LoggedBy = authenticationContext.User.UserId,
LoggedOn = DateTime.Now
};
ctx.ProductLogs.InsertOnSubmit(productLog);
ctx.SubmitChanges();
}
}
private void Validate(ProductManagementDataContext ctx, Guid? productId)
{
using (ValidationScope validation = new ValidationScope())
{
if (string.IsNullOrEmpty(this.DropDownListCategory.SelectedValue))
validation.Error("The product category should not be empty.");
if (string.IsNullOrEmpty(this.TextBoxName.Text))
validation.Error("The product name should not be empty.");
if (string.IsNullOrEmpty(this.TextBoxNumber.Text))
validation.Error("The product number should not be empty.");
Guid productIdValue = productId.HasValue ? productId.Value : Guid.NewGuid();
if (ctx.Products.Count(p => p.Id != productIdValue &&
p.Name == this.TextBoxName.Text) > 0)
validation.Error(@"The product name ""{0}"" does exist.",
this.TextBoxName.Text);
if (ctx.Products.Count(p => p.Id != productIdValue &&
p.Number == this.TextBoxNumber.Text) > 0)
validation.Error(@"The product number ""{0}"" does exist.",
this.TextBoxNumber.Text);
}
}
}
步骤 4
为批量删除创建另一个ascx模板。模板的内容非常简单,用于显示确认消息。
Are you sure to bulk delete the selected
<asp:Label ID="LabelProductCount" ForeColor="Red" runat="server" /> products?
步骤 5
为批量删除多个产品实现聚合面板。当调用*Save*方法时,网格中选定的产品将由框架自动传递。因此,实现仅专注于对传递的实体ID执行操作。
/// <summary>
/// Aggregate panel for multiple products bulk deletion.
/// </summary>
public class ProductBulkDeleteAggregatePanel : AggregatePanelPage
{
private IAuthenticationContext authenticationContext =
SpringContext.Current.GetObject<IAuthenticationContext>();
[Binding]
protected Label LabelProductCount;
/// <summary>
/// Display count of deleting products when the page is loaded.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public override void OnLoad(IRequestHandler sender, AggregatePanelPageEventArgs e)
{
this.LabelProductCount.Text = e.EntityIdEnumerable.Count().ToString();
}
/// <summary>
/// Bulk delete products by enumerable ids.
/// </summary>
/// <param name="commandArgument"></param>
/// <param name="entityIdEnumerable"></param>
public override void Save(string commandArgument,
IEnumerable<string> entityIdEnumerable)
{
if (!string.Equals("BulkDelete", commandArgument,
StringComparison.OrdinalIgnoreCase)) return;
using(TransactionScope transactionScope = new TransactionScope())
using (ProductManagementDataContext ctx =
DataContextFactory.Create<ProductManagementDataContext>())
{
Guid[] productIdArray = entityIdEnumerable.Select
(entityId => new Guid(entityId)).ToArray();
IEnumerable<Product> products = ctx.Products.Where
(p => productIdArray.Contains(p.Id) &&
p.ApplicationId == authenticationContext.ApplicationId);
ctx.Products.DeleteAllOnSubmit(products);
ctx.SubmitChanges();
transactionScope.Complete();
}
}
}
步骤 6
XML配置产品管理Web Form。*ObjectId*是Web Form的唯一键,用于作为URI访问Web Form:~/[ObjectId]/DynamicPage.svc。
<?xml version="1.0" encoding="utf-8" ?>
<Page xmlns="http://www.rapidwebdev.org/schemas/dynamicpage"
ObjectId="ProductManagement"
Type="ProductManagement.Web.ProductDynamicPage, ProductManagement">
<Title>Product Management</Title>
<PermissionValue>ProductManagement</PermissionValue>
<Panels>
<QueryPanel HeaderText="Query">
<TextBox FieldName="Name" Label="Name: " />
<TextBox FieldName="Number" Label="Number: " />
<ComboBox FieldName="CategoryId"
Label="Category: "
Editable="false"
ForceSelection="true"
FieldValueType="System.Guid">
<DynamicDataSource TextField="Name"
ValueField="Id"
Url="/Services/ConcreteDataService.svc/json/
FindByKeyword?concreteDataType=ProductCategory&limit=50" />
</ComboBox>
</QueryPanel>
<ButtonPanel ButtonAlignment="Left">
<Button CommandArgument="New" Type="Button" Text="Add" />
<Button CommandArgument="BulkDelete" Type="Button" Text="Bulk Delete">
<GridSelectionRequired WarningMessage=
"Please select the deleting products." />
</Button>
<Button CommandArgument="Print" Type="Button" Text="Print" />
<Button CommandArgument="DownloadExcel" Type="Button" Text="Export Excel" />
</ButtonPanel>
<GridViewPanel HeaderText="Query Results"
EntityName="Product"
EnabledCheckBoxField="true"
PageSize="25"
PrimaryKeyFieldName="Id"
DefaultSortField="LastUpdatedOn"
DefaultSortDirection="DESC">
<ViewButton />
<EditButton />
<DeleteButton />
<Fields>
<Field FieldName="Number" HeaderText="Number" />
<Field FieldName="Name" HeaderText="Name" />
<Field FieldName="CategoryId" HeaderText="Category" Width="80">
<Transform-Callback Type=
"RapidWebDev.Platform.Web.DynamicPage.
GridViewFieldValueTransformCallback.ShowConcreteDataName,
RapidWebDev.Platform"/>
</Field>
<Field FieldName="Manufactory" HeaderText="Manufactory" />
<Field FieldName="LastUpdatedBy" HeaderText="Updated By" Align="Center">
<Transform-Callback Type="RapidWebDev.Platform.Web.DynamicPage.
GridViewFieldValueTransformCallback.ShowUserDisplayName,
RapidWebDev.Platform"/>
</Field>
<Field FieldName="LastUpdatedOn" HeaderText="Updated On" Align="Center"
Width="150" />
<RowView FieldName="Description" />
</Fields>
</GridViewPanel>
<DetailPanel HeaderText="Product Detail"
ShowMessageAfterSavedSuccessfully="false">
<Type>ProductManagement.Web.ProductDetailPanel, ProductManagement</Type>
<SkinPath>~/Templates/ProductManagement/Product.ascx</SkinPath>
<SaveAndAddAnotherButton IsFormDefaultButton="true" />
<SaveAndCloseButton />
<CancelButton />
</DetailPanel>
<AggregatePanel HeaderText="Product Bulk Deletion Confirmation"
CommandArgument="BulkDelete"
ShowMessageAfterSavedSuccessfully="true">
<Type>ProductManagement.Web.ProductBulkDeleteAggregatePanel,
ProductManagement</Type>
<SkinPath>~/Templates/ProductManagement/ProductBulkDeleteAggregatePanel.ascx
</SkinPath>
<SaveButton />
<CancelButton />
</AggregatePanel>
</Panels>
</Page>
最后,Web Form在URL:*http://[host]:[port]/ProductManagement/DynamicPage.svc*下工作。已有文章详细介绍了如何在RapidWebDev中实现完整的Product Management系统。
权限如何工作
正如我们在XML配置中看到的,Web Form有一个*PermissionValue*段。实际上,有许多权限值从此值派生。例如,“ProductManagement
”配置为Web Form。“ProductManagement.Add
”是添加新产品的权限。“ProductManagement.Update
”是编辑现有产品的权限。“ProductManagement.[CommandArgument]
”是特殊按钮和具有该命令参数的聚合面板的权限。权限由框架智能控制,因此开发人员无需执行任何操作。
有一个名为RapidWebDev.UI.IPermissionBridge
的接口,用于UI框架与外部系统集成。当框架调用*HasPermission*方法来检查当前用户是否具有某个行为的权限时,框架会检查渲染UI交互元素和回调UI实现的权限。
/// <summary>
/// Returns true if the current user has any permissions in specified permission.
/// </summary>
/// <param name="permissionValue">Permission value.</param>
/// <returns>Returns true if the current user has any
/// permissions in specified permission.</returns>
bool HasPermission(string permissionValue);
最后
我将在本系列的后续文章中详细介绍RapidWebDev
UI框架。UI框架只是RapidWebDev
解决方案的一个重要组成部分,但不是全部。
什么是RapidWebDev
RapidWebDev
是一个基础设施,可帮助工程师轻松高效地在Microsoft .NET中开发企业软件解决方案。它由一个可扩展且可维护的Web系统架构组成,并提供一套通用的业务模型、API和服务,作为几乎所有业务解决方案开发所需的基础功能。因此,当工程师在RapidWebDev
中开发解决方案时,他们可以获得许多可重用的现成组件,从而可以更专注于业务逻辑的实现。在实践中,与传统的ASP.NET开发相比,我们可以节省50%以上的时间来开发高质量、高性能的业务解决方案。