Develop web forms by RapidWebDev UI (1)

The series of articles introduces an innovative architect to develop business web forms in enterprise software development which is better performance, higher productivity, more configurability and easier maintainability than traditional either ASP.NET or MVC development.

Facing Problems

When we develop a web form in ASP.NET or MVC, we need work on,
1) create view template and code behind (controller)
2) style adjustment
3) write event handlers for all actions
4) control visibility of web controls for different actions manually
5) permission integration manually
6) hard to write unit tests (not for MVC)
7) something else

With all of those staff, the source code of developed web forms are very complex usually by mixed logics of webcontrols' visibility control, business logics, style management, permission check etc. The web forms are less productive, bug-ness and hard maintainable.

New Solution

RapidWebDev is a new UI framework designed to solve the facing problems. The principle of design is to abstract requirement of web forms for usual business scenarios and define xml schema to configure web forms so that user behavior, style, permission are all managed by the framework. And some interfaces of the framework are required to be implemented which are callback for user behaviors. E.G. there is a query panel with filters configured. When an user clicks query button in query panel, method Query of the implementation to interface IDynamicPage will be invoked with query parameters. With this design,
  1. web forms are configurable and maintainable.
  2. code complexity is decreased obviously.
  3. implementations are reusable and testable
  4. consolidate ui style by the framework

The roadmap of introduction to RapidWebDev UI architect in this article is to
  1. introduce web form structure/layout
  2. communication between components in a web form
  3. what're required to develop a web form
  4. a sample product management application
  5. how permission works

This article doesn't introduce the detail implementation of the architect but focusing on high level innovative points. I will introduce the details in later articles in the series.

Web Form Structure

We abstract generic panel types for usual business scenarios - "query panel" to set query filters; "grid panel" to display queried records; "detail panel" to create/update/view/delete a single record; "button panel" to configure buttons for custom actions; "aggregate panels" for bulk operate multiple records selected in grid etc.

Let's go through some usual business scenarios in enterprise software.
  • in a product management web form, some users can create a draft product, approve products, export or forward them. There should be a "query panel" for users to setup filters, a "grid panel" to display queried products in list, a "detail panel" to create/update/view a single product detail information, an approve panel ("aggregate panel") to approve selected products in the grid and a forward panel ("aggregate panel") to forward selected products to someone else by mail etc.
  • in a stock-in product web form, users can scan product number in a scanning panel ("detail panel" with a product number input textbox) and get scanned products displayed in a temporary "grid panel". Users can submit multiple selected scanned products in grid and get a stock-in sheet ("aggregate panel").
  • in a news management web form, users can setup new query filters in "query panel" and get them displayed in "grid panel". Users can add a new news or select a exist news in grid to update, delete or view detail information in "detail panel".

Let's preview these panel types as the following screenshot,

Query, Button and Grid panel


Detail panel


Aggregate panel

Communication b/w Panels

The communication b/w panels is described as the following workflow chart,


Query Panel
When an user clicks Query button in query panel, the query panel grabs all query filters and sends an asynchronous request to web server and pull records rendered into grid panel.

Grid Panel
Display queried records. There are three buttons can be configured for each row in the grid, they're Edit, View and Delete. When the user clicks Edit/View button in a row, the detail panel is shown up. In Edit mode, the grid refreshes automatically after the user saves the editing record successfully. The grid supports column resizing, show/hide column, sorting, paging, row preview automatically.

Button Panel
Configure custom buttons in web form that each button is assigned with a command argument. When a button is clicked, the aggregate panel with the same command argument will be displayed. But there are three command argument are predefined which not relates to aggregate panels. They're Add, Print and DownloadToExcel. A blank detail panel is displayed when Add button is clicked. Print and DownloadToExcel button is used to print or export all grid records (include in other pagination) to Excel without writing any code.

Detail Panel
Create/update/view a single record. Grid panel refreshes automatically after the user saves in detail panel.

Aggregate Panel
Any custom operations. Grid panel refreshes automatically after the user saves in aggregate panel.

What're required to develop a web form

Xml configuration and implementation of interface IDynamicPage is required for a web form. In xml configuration, we should configure query filters, grid fields, buttons, detail panel and aggregate panels. Don't worry about dynamic data in static xml configuration. Xml configuration allows to configure callback processors.

When we need detail panel in a web form, we need to develop a ascx template without code behind and implements interface IDetailPanel. The ascx template is used to render the web controls in detail panel as the following screenshot. The IDetailPanel implementation integrates the ascx template for business logics.


The aggregate panel development is the same to detail panel.

A sample product management application

We have requirement of a product management web form as following,
  1. create a new product
  2. edit/view an existed product
  3. delete a single product
  4. bulk delete multiple selected products
  5. print queried products
  6. export queried product to Excel
  7. show the product changing logs when edit/view a product

Step 1, implement dynamic page interface which is used for querying products and deleting a single product. You see in the method Query, actually we don't assemble query expression. The argument "parameter" passed from the framework can be converted to LinqPredicate directly. The framework not only use query panel xml configuration to render UI but also assemble query expression. So when we have requirement to change query filters, we only need to change the xml configuration without compilation.
/// <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();
        }
    }
}

Step 2, create a ascx template for product detail panel. The template is used to render the form to add/update/view a product. The ASP.NET server control will be bound to the member declaration of detail panel implementation class automatically by 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>

Step 3, implement detail panel interface which is used for add/update/view a single product. The references of data members with custom attribute Binding are resolved by the framework from ascx template automatically. The data member name should be the same as the server control ID in the template. The argument parentControlPath of Binding(string parentControlPath) indicates which ASP.NET ITemplate control includes the control.
/// <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);
        }
    }
}

Step 4, create another ascx template for bulk deletion. The content of the template is quite simple which is used to display a confirmation message.
Are you sure to bulk delete the selected 
<asp:Label ID="LabelProductCount" ForeColor="Red" runat="server" /> products?

Step 5, implement aggregate panel for bulk deleting multiple products. When the method Save is invoked, the selected products in grid will be passed in automatically by the framework. So the implementation is only focused on operations against the passing entity IDs.
/// <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();
        }
    }
}

Step 6, xml configure product management web form. The ObjectId is an unique key of a web form which is used to access the web form as URI: ~/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&amp;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>


Finally, the the web form works in the URL: http://host:port/ProductManagement/DynamicPage.svc. There is an existed article introducing the detail on implementing a whole product management system in RapidWebDev.

How permission works

As we seen in xml configuration, there is a segment PermissionValue for the web form. Actually there are many permission values derived from the value. E.G. "ProductManagement" is configured for the web form. "ProductManagement.Add" is the permission for adding a new product. "ProductManagement.Update" is the permission for editing an existed product. "ProductManagement.CommandArgument" is the permission for special button and aggregate panel with that command argument. The permission is intelligently controlled by the framework so that developers don't need to do anything.

There is an interface RapidWebDev.UI.IPermissionBridge which is used for the UI framework to integrate with external systems. The method HasPermission is invoked by the framework to check whether the current user having the permission on a behavior. The framework checks permission on both rendering UI interactive elements and callback UI implementation.
/// <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);

Last edited Mar 7, 2010 at 12:15 PM by eungeliu, version 2

Comments

No comments yet.