Blog: Posts from March, 2012

Labels
AJAX(112) App Studio(7) Apple(1) Application Builder(245) Application Factory(207) ASP.NET(95) ASP.NET 3.5(45) ASP.NET Code Generator(72) ASP.NET Membership(28) Azure(18) Barcode(2) Barcodes(3) BLOB(18) Business Rules(1) Business Rules/Logic(140) BYOD(13) Caching(2) Calendar(5) Charts(29) Cloud(14) Cloud On Time(2) Cloud On Time for Windows 7(2) Code Generator(54) Collaboration(11) command line(1) Conflict Detection(1) Content Management System(12) COT Tools for Excel(26) CRUD(1) Custom Actions(1) Data Aquarium Framework(122) Data Sheet(9) Data Sources(22) Database Lookups(50) Deployment(22) Designer(177) Device(1) DotNetNuke(12) EASE(20) Email(6) Features(101) Firebird(1) Form Builder(14) Globalization and Localization(6) How To(1) Hypermedia(2) Inline Editing(1) Installation(5) JavaScript(20) Kiosk(1) Low Code(3) Mac(1) Many-To-Many(4) Maps(6) Master/Detail(36) Microservices(4) Mobile(63) Mode Builder(3) Model Builder(3) MySQL(10) Native Apps(5) News(18) OAuth(9) OAuth Scopes(1) OAuth2(13) Offline(20) Offline Apps(4) Offline Sync(5) Oracle(11) PKCE(2) Postgre SQL(1) PostgreSQL(2) PWA(2) QR codes(2) Rapid Application Development(5) Reading Pane(2) Release Notes(183) Reports(48) REST(29) RESTful(29) RESTful Workshop(15) RFID tags(1) SaaS(7) Security(81) SharePoint(12) SPA(6) SQL Anywhere(3) SQL Server(26) SSO(1) Stored Procedure(4) Teamwork(15) Tips and Tricks(87) Tools for Excel(2) Touch UI(93) Transactions(5) Tutorials(183) Universal Windows Platform(3) User Interface(338) Video Tutorial(37) Web 2.0(100) Web App Generator(101) Web Application Generator(607) Web Form Builder(40) Web.Config(9) Workflow(28)
Archive
Blog
Posts from March, 2012
Saturday, March 3, 2012PrintSubscribe
Read-Only Database Applications

Data controllers of Code On Time web applications are equipped with a flexible set of data manipulation commands. Users can insert, duplicate, update, and delete data served by any data controller. The exceptionally simple and powerful action state machine makes very complex action sequences possible.

No doubt that the application administrator will be quite happy! What if you have a requirement to allow read-only access to data for a certain group of users or even a certain user account?

Let’s inventory the scope of work that needs to be performed.

Consider the following application built on top of the Northwind sample. Zero effort is required to enable full data editing in the Products list.

'Products'  list with all data manipulation capabilities enabled requires zero configuration

The context menu of data rows in the grid view offers Edit, Delete, Duplicate, and New commands.

Context menu in a grid view

The action bar offers New Products, Edit, Delete, and Import options.

Action bar offers New, Edit, Delete, and Import options.

If a user clicks on the link in the first column of any product row or chooses Select option from the context menu then the form view will be activated. Users are able to start editing or delete the row. Action bar also offers New Products and Import actions.

Form view allows  to start editing, delete, create new, or import a batch of records.

Code On Time web applications allow specifying a list of roles that are authorized to execute an action. The partial action state machine of the data controller Products is presented below.

<actions>
  <actionGroup id="ag1" scope="Grid">
    <action id="a1" commandName="Select" commandArgument="editForm1" />
    <action id="a2" commandName="Edit" />
    <action id="a3" commandName="Delete" />
    <action id="a6" />
    <action id="a7" commandName="Duplicate" commandArgument="createForm1" />
    <action id="a8" commandName="New" commandArgument="grid1" />
  </actionGroup>
  <actionGroup id="ag2" scope="Form">
    <action id="a1" commandName="Edit" />
    <action id="a2" commandName="Delete" />
    . . . . .
  </actionGroup>
  <actionGroup id="ag3" scope="ActionBar" headerText="New" flat="true">
    <action id="a1" commandName="New" commandArgument="createForm1" 
            cssClass="NewIcon" />
  </actionGroup>
  <actionGroup id="ag4" scope="ActionBar" headerText="Edit/Delete" flat="true">
    <action id="a1" whenKeySelected="true" commandName="Edit" 
            commandArgument="editForm1" cssClass="EditIcon" whenView="grid1" />
    <action id="a2" whenKeySelected="true" commandName="Delete" 
            cssClass="DeleteIcon" whenView="grid1" />
  </actionGroup>
  <actionGroup id="ag5" scope="ActionBar" headerText="Actions">
    . . . . .
    <action id="a6" commandName="Import" commandArgument="createForm1" />
    . . . . .
  </actionGroup>
  . . . . .
</actions>

You can modify the data controller baseline and specify the “roles” attribute for each of the ten actions above.

<action id="a2" commandName="Edit" roles="Administrators" />

You can also use the  Project Designer and edit the the Roles property for each action there.

Action 'Roles' property in Project Designer

That may not be  a huge challenge in a small project but if you do need to configure a few dozen data controllers then you are probably thinking that there must be a better way.

It is possible to customize each data controller at runtime with the help of data controller virtualization.

Enable shared business rules in the Business Logic Layer settings of your web application and implement the SharedBusinessRules class as shown below.

C#:

using System;
using System.Data;
using System.Collections.Generic;
using System.Linq;
using MyCompany.Data;
using System.Xml.XPath;
using System.Xml;

namespace MyCompany.Rules
{
    public partial class SharedBusinessRules : MyCompany.Data.BusinessRules
    {
        public override bool SupportsVirtualization(string controllerName)
        {
            return !UserIsInRole("Administrators");
        }

        public override void VirtualizeController(string controllerName, 
            XPathNavigator navigator, XmlNamespaceManager resolver)
        {
            string[] restrictedCommands = { "Edit", "New", "Delete", "Duplicate", "Import" };
            XPathNodeIterator actionIterator = navigator.Select("//c:action", resolver);
            while (actionIterator.MoveNext())
            {
                string commandName = actionIterator.Current.GetAttribute(
                    "commandName", String.Empty);
                if (restrictedCommands.Contains(commandName))
                    actionIterator.Current.CreateAttribute(
                        String.Empty, "roles", String.Empty, "Administrators");
            }
        }
    }
}

Visual Basic:

Imports MyCompany.Data
Imports System
Imports System.Collections.Generic
Imports System.Data
Imports System.Linq
Imports System.Xml
Imports System.Xml.XPath

Namespace MyCompany.Rules

    Partial Public Class SharedBusinessRules
        Inherits MyCompany.Data.BusinessRules

        Public Overrides Function SupportsVirtualization(controllerName As String) As Boolean
            Return Not UserIsInRole("Administrators")
        End Function

        Public Overrides Sub VirtualizeController(controllerName As String,
            navigator As XPathNavigator, resolver As XmlNamespaceManager)
            Dim restrictedCommands() As String =
                {"Edit", "New", "Delete", "Duplicate", "Import"}
            Dim actionIterator As XPathNodeIterator = navigator.Select("//c:action", resolver)
            While (actionIterator.MoveNext())
                Dim commandName As String = actionIterator.Current.GetAttribute(
                    "commandName", String.Empty)
                If (restrictedCommands.Contains(commandName)) Then
                    actionIterator.Current.CreateAttribute(
                        String.Empty, "roles", String.Empty, "Administrators")
                End If
            End While

        End Sub
    End Class
End Namespace

The method SupportsVirtualization returns True if the user performing the current web request is not an administrator.

Method VirtualizeController scans the list of actions and assigns “roles” attribute to actions that initiate commands Edit, New, Delete, Duplicate, and Import. The actual data controller file is not changed. The changes are discarded as soon as the current request is processed.

Notice that we “hide” the actions from the user account by assigning Administrators value to the “roles” attribute. You can assign any value instead of Administrators as long as the value does not represent the role assigned to the non-administrative user account. You can also delete the action from the data controller. The outcome will be exactly the same.

Run the application and login with the user account user/user123%. Notice that the ability to edit data is not available anymore.

Data controller virtualization allows implementing a single method that assignes a 'roles' attribute to each action that must have a restricted access.

Saturday, March 3, 2012PrintSubscribe
Virtualization of Data Controllers

Code On Time applications rely on data controller descriptors to determine the expected application behavior at runtime.  A typical data controller XML file packages information about fields, data views, action state machine, and optional linked business rules.

The application framework interprets this data. The server components of the framework parse the XML files and prepare internal structures to process requests to select, update, insert, and delete data. The AJAX client portion of the application framework renders data views and available state machine actions in web browsers.

From the application framework perspective, the physical location of data controller files is unknown. Business rules classes are not aware and don’t care about the location of data controllers.

Web Site Factory applications have all data controller files stored in ~/Controllers folder.

Data controllers in a Web Site Factory application

All other project types are implemented as a Visual Studio solution with several projects. The core application framework and data controller files are located in the Class Library project of the solution. The data controllers are marked as resources. The entire text of XML files is compiled into the DLL of the class library.

The next screen shot shows a class library in a SharePoint Factory project.

Data controllers in SharePoint Factory application

The application framework tries the following sequence while loading a data controller in memory.

  1. Ask the Controller class implementation to return the data controller content stream by name. The default implementation returns nothing.
  2. Try loading the data controller from the binary resources of the application. All project types other than Web Site Factory will return the resource stream of a data controller.
  3. Try loading the data controller file from ~/Controllers folder. This applies to Web Site Factory only.

Virtualization of a data controller is a replacement of an entire data controller or some parts of it at runtime.

If you want to store an application data controller outside of application code base then add the following partial class definition. Your class will complement the default application framework implementation of controller functionality.

C#:

using System;
using System.IO;

namespace MyCompany.Data
{

    public partial class Controller
    {
        public override Stream GetDataControllerStream(string controller)
        {
            string fileName = String.Format(@"c:\clients\Acme\{0}.xml", controller);
            if (File.Exists(fileName))
                return new MemoryStream(File.ReadAllBytes(fileName));
            return DefaultDataControllerStream;
        }
    }
}

Visual Basic:

Imports Microsoft.VisualBasic
Imports System.IO

Namespace MyCompany.Data

    Partial Public Class Controller
        Public Overrides Function GetDataControllerStream(controller As String) As Stream
            Dim fileName = String.Format("c:\\clients\Acme\{0}.xml", controller)
            If (File.Exists(fileName)) Then
                Return New MemoryStream(File.ReadAllBytes(fileName))
            End If
            Return DefaultDataControllerStream
        End Function
    End Class

End Namespace

The sample above loads a customized version of the data controller Customers if the file is found in C:\Clients\Acme folder.  Otherwise the application will proceed to load the default data controller defined in the application.

If you have a multi-tenant web application and desire to offer customized data controllers for a specific group of users then consider using this method of customization. You can inspect the incoming web request or session variables and identify the user group. Load the correct data controller based on that info.

We recommend using this method of data controller virtualization when you need to branch the data controller definition.

You can also customize the contents of a data controller after the file has been loaded by the application framework in the memory and is ready to be used in the web request processing. This method of virtualization involves changing a portion of a data controller definition while leaving the rest of it intact.

Implement either a dedicated business rules class for your controller or create a shared business rules class to have all application data controllers share a common functionality.

Let’s create a partial data controller virtualization sample with shared business rules.

Select your project on the start page of the web application generator and choose Settings, click Business Logic Layer, and select Shared Business Rules. Check the box that enables shared business rules. Click Finish and generate your project.

The class implementing shared business rules will either be in ~/App_Code/Rules folder of your project or in the ~/Rules folder of the solution class library.

Shared Business Rules class implementation in a Web Site Factory project

Make sure that any existing custom business rules inherit from the class [YourNamespace].Rules.SharedBusinessRules. Replace [YourNamespace] with the namespace of your project.

The following implementation of shared business rules alters data controller Customers based on the current user interface culture.

If the culture is “en-US” then the Region field label is changed to State; the Postal Code field label is changed to Zip Code; and all instances of Country data field are “hidden” from the application users.

Notice that the changes do not effect the actual file that contains the definition of Customers data controller.

C#:

using System;
using System.Data;
using System.Collections.Generic;
using System.Linq;
using MyCompany.Data;
using System.Xml.XPath;
using System.Xml;

namespace MyCompany.Rules
{
    public partial class SharedBusinessRules : MyCompany.Data.BusinessRules
    {
        public override bool SupportsVirtualization(string controllerName)
        {
            if (controllerName == "Customers")
                return true;
            else
                return false;
        }

        public override void VirtualizeController(string controllerName,
            XPathNavigator navigator, XmlNamespaceManager resolver)
        {
            if (controllerName == "Customers")
            {
                if (System.Threading.Thread.CurrentThread.CurrentUICulture.Name == "en-US")
                {
                    // Change "Region" label to "State" for users from the USA
                    XPathNavigator regionLabel = navigator.SelectSingleNode(
                        "/c:dataController/c:fields/c:field[@name='Region']/@label",
                        resolver);
                    if (regionLabel != null)
                        regionLabel.SetValue("State");
                    // Change "Postal Code" label to "Zip Code" for users from the USA
                    XPathNavigator postalCodeLabel = navigator.SelectSingleNode(
                        "/c:dataController/c:fields/c:field[@name='PostalCode']/@label",
                        resolver);
                    if (postalCodeLabel != null)
                        postalCodeLabel.SetValue("Zip Code");
                    // Mark all data fields named "Country" as "hidden"
                    XPathNodeIterator countryFieldIterator = navigator.Select(
                        "//c:dataField[@fieldName='Country']",
                        resolver);
                    while (countryFieldIterator.MoveNext())
                    {
                        XPathNavigator dataFieldNav = countryFieldIterator.Current;
                        if (dataFieldNav.MoveToAttribute("hidden", String.Empty))
                            dataFieldNav.SetValue("true");
                        else
                            dataFieldNav.CreateAttribute(
                                String.Empty, "hidden", String.Empty, "true");
                    }
                }
            }
        }
    }
}

Visual Basic:

Imports MyCompany.Data
Imports System
Imports System.Collections.Generic
Imports System.Data
Imports System.Linq
Imports System.Xml.XPath
Imports System.Xml

Namespace MyCompany.Rules

    Partial Public Class SharedBusinessRules
        Inherits MyCompany.Data.BusinessRules

        Public Overrides Function SupportsVirtualization(controllerName As String) As Boolean
            If controllerName = "Customers" Then
                Return True
            Else
                Return False
            End If

        End Function

        Public Overrides Sub VirtualizeController(controllerName As String,
                                                  navigator As XPathNavigator,
                                                  resolver As XmlNamespaceManager)
            If (controllerName = "Customers") Then
                If (System.Threading.Thread.CurrentThread.CurrentUICulture.Name = "en-US") Then
                    ' Change "Region" label to "State" for users from the USA
                    Dim regionLabel As XPathNavigator = navigator.SelectSingleNode(
                        "/c:dataController/c:fields/c:field[@name='Region']/@label",
                        resolver)
                    If (Not regionLabel Is Nothing) Then
                        regionLabel.SetValue("State")
                    End If
                    ' Change "Postal Code" label to "Zip Code" for users from the USA
                    Dim postalCodeLabel As XPathNavigator = navigator.SelectSingleNode(
                        "/c:dataController/c:fields/c:field[@name='PostalCode']/@label",
                        resolver)
                    If (Not postalCodeLabel Is Nothing) Then
                        postalCodeLabel.SetValue("Zip Code")
                    End If
                    ' Mark all data fields named "Country" as "hidden"
                    Dim countryFieldIterator As XPathNodeIterator = navigator.Select(
                        "//c:dataField[@fieldName='Country']",
                        resolver)
                    While (countryFieldIterator.MoveNext())
                        Dim dataFieldNav = countryFieldIterator.Current
                        If (dataFieldNav.MoveToAttribute("hidden", String.Empty)) Then
                            dataFieldNav.SetValue("true")
                        Else
                            dataFieldNav.CreateAttribute(
                                String.Empty, "hidden", String.Empty, "true")
                        End If
                    End While
                End If
            End If
        End Sub

    End Class
End Namespace

This is the screen shot that shows the effect of the shared business rules. User interface is altered at runtime when English (United States) localization is selected.

'Customers' data controller affected by virtualization

This screen shot shows localized version of the web application without dynamic customization of user interface.

'Customers' data controller not affected by virtualization

The portion of the data controller definition that is being changed at runtime is shown next.

<fields>
  . . . . .
  <field name="Region" type="String" label="Region" />
  <field name="PostalCode" type="String" label="Postal Code" />
  <field name="Country" type="String" label="Country" />
  . . . . . 
</fields>
<views>
  <view id="grid1" type="Grid" commandId="command1" label="Customers">
    <headerText>$DefaultGridViewDescription</headerText>
    <dataFields>
      . . . . .
      <dataField fieldName="Country" columns="15" />
      . . . . .
    </dataFields>
  </view>
  <view id="editForm1" type="Form" commandId="command1" label="Review Customers">
    <headerText>$DefaultEditViewDescription</headerText>
    <categories>
      <category id="c1" headerText="Customers" newColumn="true">
        <description><![CDATA[$DefaultEditDescription]]></description>
        <dataFields>
          . . . . .
          <dataField fieldName="Country" columns="15" />
          . . . . .
        </dataFields>
      </category>
    </categories>
  </view>
  <view id="createForm1" type="Form" commandId="command1" label="New Customers">
    <headerText>$DefaultCreateViewDescription</headerText>
    <categories>
      <category id="c1" headerText="New Customers" newColumn="true">
        <description><![CDATA[$DefaultNewDescription]]></description>
        <dataFields>
          . . . . .
          <dataField fieldName="Country" columns="15" />
          . . . . .
        </dataFields>
      </category>
    </categories>
  </view>
</views>
Friday, March 2, 2012PrintSubscribe
Understanding the Project

The Northwind database is a typical mail order management system.

The core of the database is composed of two tables: Orders and Order Details. Each sale is recorded in the Orders table. Items purchased are recorded in the Order Details table.

Orders and Order Details tables from Northwind database

Orders table has references to Customers, Employees, and ShippersOrders table also contains information about Order Date, Required Date, Shipped Date, Freight Amount, and the shipping details.

Orders table and foreign keys in Northwind database

Order Details table refers to Products and indirectly references Categories and Suppliers. Order Details will contain a few fields taken from all of these foreign key relationships.

image

This is the baseline version of the orders screen, created automatically by the web application generator.

Baseline version of Orders screen

We will create a custom version of this form, to make the order entry process as friendly as possible.