Introduction
This article briefly describes an n-tier application that I have written using VB.NET. The application has a somewhat obscure purpose which is to provide electronic signatures in a SCADA system called RSView32.
However of interest to readers might be how I configure each tier of the application and also the way in which the presentation tier hosts MSHTML. There has been a lot written about hosting MSHTML but most of it seems to rely on windows.external calls to provide the interface between
elements in the MSHTML pane and the application. My application takes a different approach which is to use a one to one relationship whereby
my .NET presentation objects host individual DHTML objects within the MSHTML object model.
I must apologize for the cursory nature of this article but I do hope to extend it
considerably if there is much interest in what I have done. I haven't included screendumps but they can be viewed in the software description on my website.
Please bear in mind that these screendumps are not web pages. Different elements in the MSHTML pane are in fact connected to different business objects
in different processes with remoting links. When a business object fires an event individual portions of the MSHTML pane get refreshed rather than the whole pane.
Background
The ideas for the presentation layer developed out of a reporting system which I prototyped using Internet Explorer whereby when html elements were expanded they fired off web service
calls, transformed the results with xslt and then inserted them into the HTML document. I found it hard to handle the complexity with
JavaScript and started to think
about how I might embed .NET code into the Internet Explorer web page to make it 'live'. Once I discovered I could access the entire object model of a MSHTML page programmatically I
realized that the solution was to embed the DHTML objects from this model into a .NET application.
Description
dkTagSet is a suite of programs that provide embedded Electronic Signatures and displayed documentation in an RSView32 SCADA System.
dkTagSet is used to enable the user to set tags from a SCADA system. When a user elects to set or edit tags it brings up a screen which displays documentation to the user and requires the user to enter a number of Electronic Signatures. Once these signatures have been added the required tags are set.
dkTagSet displays detailed information to the user about the tags being set, including PLC address and tag descriptions.
Once configured in an RSView32 project the signatures can be reconfigured by the user from the runtime environment without having to re edit screens. This re configuration consists of configuring both the documentation displayed by the user and also the confirmations required for the action. All re configuration is protected by electronic signatures and all re configuration actions can have documentation associated with them.
dkTagSet maintains a complete audit trail of all actions performed with it. This audit trail includes both tag setting actions and also system reconfiguration. The audit trail is viewable from the front end.
Architecture
dkTagSet has an n-tier architecture. An n-tier application is divided into a number of separate tiers each of which carries out a different class of function. The tiers that dkTagSet is separated into are as follows:
Data Tier
The Data Tier consists of an SQL Server database. All actions performed by the system are logged into a set of tables in this database. It has simple stored procedures called by the data access tier to insert and update data.
The architecture allows for different databases to be used.
Data Access Tier
The Data Access Tier is a set of classes written with the .NET framework.
It uses the proprietary driver to communicate with the SQL Server DataBase although in fact this driver is configured so other databases would be possible.
The Data Tier is configured by an XML configuration file which defines a number of different data access objects. When a data access object is requested from the data tier with a particular name it uses this XML file to look up and configure the required object. The XML configuration file configures the following:
- The .NET type and assembly of the object
- An XML Schema that defines the structure of a dataset that is used to communicate to and from the object
- The driver to be used to connect to the database
- The rules used to read data from and write data to the database
A standard interface is used to talk to all data access objects. This interface uses DataSets.
When data is required from the data access object a dataset defines the data that is required. The Data Access Object uses its configured rules to generate queries to obtain data from the database and then returns the data in a dataset.
When the data object is required to write data to the database the data to be written is passed to it in a dataset.
The data object uses the rules configured for it to write this data back to the database.
Data Tier configuration example
This XML Element configures a data access object (in this case the data for a set of confirmations):
<confirmations>
<dalObject
type='dkcs.dal.dalConfirmations.dkConfirmationsDataObject'
assemblyFile='dal.dll'>
<schema path='schemas/schemaConfirmations.xsd' />
<getDataSet>
<table name="confirmations">
<table name="confirmation"
queryString="select * from confirmation where (1=0) "
order=' order by ord asc'>
<linkFields parentTableName="confirmations">
<linkField name="parentGuid"
parentFieldName="guid" type='string' />
</linkFields>
</table>
</table>
<dataSet queryString=
"select * from confirmations where (1 = 9) ">
<or>
<table tableName='confirmations' operator='or'>
<selector leftField='parentGuid' conJoin='='
tableField='parentGuid' type='string' />
<selector operator='or' leftField='guid' conJoin='='
tableField='guid' type='string' />
</table>
</or>
</dataSet>
</getDataSet>
<storeDataset>
<update tableName='confirmations'>
<insertCommand commandText='insertConfirmations'>
<parameter dbType='16'
parameterName="@parentGuid" sourceColumn='parentGuid' />
<parameter dbType='16'
parameterName="@guid" sourceColumn='guid' />
<parameter dbType='16'
parameterName="@confirmSet" sourceColumn='confirmSet' />
<parameter dbType='16'
parameterName="@zone" sourceColumn='zone' />
<parameter dbType='16'
parameterName="@dalObject" sourceColumn='dalObject' />
<parameter dbType='6'
parameterName="@dateCreated" sourceColumn='dateCreated' />
</insertCommand>
<updateCommand commandText='updateConfirmations'>
<parameter dbType='16'
parameterName="@parentGuid" sourceColumn='parentGuid' />
<parameter dbType='16'
parameterName="@guid" sourceColumn='guid' />
<parameter dbType='16'
parameterName="@confirmSet" sourceColumn='confirmSet' />
<parameter dbType='16'
parameterName="@zone" sourceColumn='zone' />
<parameter dbType='16'
parameterName="@dalObject" sourceColumn='dalObject' />
<parameter dbType='16'
parameterName="@confirmResult"
sourceColumn='confirmResult' />
<parameter dbType='6'
parameterName="@dateConfirmed"
sourceColumn='dateConfirmed' />
</updateCommand>
</update>
<update tableName='confirmation'>
<insertCommand commandText='insertConfirmation'>
<parameter dbType='16'
parameterName="@parentGuid" sourceColumn='parentGuid' />
<parameter dbType='16'
parameterName="@guid" sourceColumn='guid' />
<parameter dbType='10'
parameterName="@ord" sourceColumn='ord' />
<parameter dbType='16'
parameterName="@confirmType" sourceColumn='confirmType' />
<parameter dbType='16'
parameterName="@attemptGuid" sourceColumn='attemptGuid' />
</insertCommand>
<updateCommand commandText='updateConfirmation'>
<parameter dbType='16'
parameterName="@guid" sourceColumn='guid' />
<parameter dbType='16'
parameterName="@attemptGuid" sourceColumn='attemptGuid' />
<parameter dbType='16'
parameterName="@failGuid" sourceColumn='failGuid' />
</updateCommand>
</update>
</storeDataset>
<configuration>
<confirmSets
xmlns="http://www.dkcs.ws/dk400/namespaces/confirmations">
<confirmations confirmSet="defaultConfirm">
<confirmation ord='1' confirmType="operatorConfirm" />
</confirmations>
<confirmations confirmSet="operatorConfirm">
<confirmation ord='1' confirmType="operatorConfirm" />
</confirmations>
<confirmations confirmSet="controllerConfirm">
<confirmation ord='1' confirmType="controllerConfirm" />
</confirmations>
<confirmations confirmSet="no id required">
<confirmation ord='1' confirmType="no id required" />
</confirmations>
<confirmations confirmSet="doubleOperatorConfirm">
<confirmation ord='1' confirmType="operatorConfirm" />
<confirmation ord='2' confirmType="operatorConfirm" />
</confirmations>
<confirmations confirmSet="administratorConfirm">
<confirmation ord='1' confirmType="administratorConfirm" />
</confirmations>
</confirmSets>
</configuration>
</dalObject>
</confirmations>
data Object Interface
All data objects conform to the following interface.
The business layer has no idea and no way of telling what data object it is actuially talking to.
Public Interface dkIDataObject
Inherits general.general.dkIObject
Overloads Function getDataSet(ByVal queryDataset _
As DataSet) As DataSet
As String) As DataSet
Sub storeDataSet(ByVal storeDataset As DataSet)
Function blankDataSet() As DataSet
Function initDataSet() As DataSet
Function updateDataset(ByVal ds As DataSet) As DataSet
Overloads Function create(ByVal createDataSet As _
DataSet) As DataSet
End Interface
Data Factory
This is the factory for data objects it creates one, configures it and then
calls a function on it for a client object:
Imports System.EnterpriseServices
Imports System.Xml.Serialization
Imports dkcs.dal.dalConfigurable
Imports dkcs.dal.dalGeneral
Imports dkcs.general.general
Namespace dal.dalFactory
<Serializable()> _
Public Class dkDataFactoryConfigurable
Inherits servicedConfigurator
Implements dkIDataObjectFactory
Public Sub New()
MyBase.new()
End Sub
Private Function getDataObject(ByVal zone As String, _
ByVal name As String) As dkIDataObject
Dim c As New dalbase.dkDataObjectConfigurable()
c.configuration = dkZoneConfiguration.zoneConfiguration( _
zone).SelectSingleNode("dalObjects").SelectSingleNode(name)
Return c
End Function
Business Tier
The Business Tier is a set of classes written with the .NET Framework.
All classes have a common interface that allows communication with the objects using .NET DataSets.
The classes are also capable of holding other business objects and returning these business objects on request.
The Business Tier is configured by an XML configuration file which defines a number of different
business objects. When a business object is requested from the business tier with a particular name
it uses this XML file to look up and configure the required object. The XML configuration file configures
the following for each business object:
- The .NET type and assembly of the object
- The names and network locations of various 'sub objects' these being other business objects and also the data access object
- Other configuration information specific to the object
Business Tier Configuration Example
This XML Element configures a Business Tier object (in this case
for a set of confirmations) the url of the dal object could make it on a different machine:
<confirmations>
<businessObject
type='dkcs.business.confirmations.confirmationsBusinessObject'
assemblyFile='business.dll' zone="xBrewery">
<objectUrls>
<objectUrl purpose='dal'
url='tcp://localhost:123/dalFactory.soap'
name='confirmations' />
<objectUrl purpose='businessSecurityProviderFactory'
url='' name='securityProvider' />
</objectUrls>
</businessObject>
</confirmations>
Business Tier Interface
This interface is implemented by all business objects objects using a business tier object
don't actually know the type of the object that they are using:
Public Interface dkIBusinessObject
Inherits dkcs.general.general.dkIObject
Inherits dkcs.general.general.dkIConfigurable
Inherits IDisposable
Property initDataset() As DataSet
Property initDataSetString() As String
Function getDataSet() As DataSet
Function getDataSet(ByVal parentDataSet As DataSet, _
ByVal tableName As String) As DataSet
Function setDataSet(ByVal ds As DataSet) _
As DataSet
Function setDataSet(ByVal ds As DataSet, _
ByVal objectName As String) As DataSet
Function blankdataset() As DataSet
Sub storedataset()
Event changeDataset(ByVal ds As DataSet)
Function getBusinessObject(ByVal name As String, _
ByVal parentDataSet As DataSet) As dkIBusinessObject
Function setBusinessObject(ByVal name As String, _
ByVal businessObject As dkIBusinessObject) As dkIBusinessObject
Property businessFactory() As dkIBusinessFactory
ReadOnly Property key()
Function command(ByVal ParamArray commandstring() _
As String) As String
ReadOnly Property businessObject() As dkIBusinessObject
End Interface
Business Factory
This is the namespace that deals with creating business objects. The businessfactory masquerades as the business
object that it has created:
Public Class myBusinessFactory
Shared Function getBusinessObject(ByVal configuration _
As Xml.XmlElement, ByVal objectName As String, _
ByVal parentDataSet As DataSet) As dkIBusinessObject
Dim o As Object = getBusinessFactory(configuration, _
objectName).getBusinessObject(configuration.SelectSingleNode( _
String.Format("objectUrls/objectUrl[@purpose='{0}']/@name", _
objectName)).Value, parentDataSet)
Return o
End Function
Overloads Shared Function getBusinessFactory(ByVal configuration _
As Xml.XmlElement, ByVal objectName As String) As _
general.dkIBusinessFactory
Dim n As Xml.XmlNode = configuration.SelectSingleNode( _
String.Format("objectUrls/objectUrl[@purpose='{0}']/@url", _
objectName))
If Not n Is Nothing Then
Dim b As dkIBusinessFactory = getBusinessFactory( _
configuration.Attributes("zone").Value, n.Value)
b.name = configuration.SelectSingleNode(String.Format( _
"objectUrls/objectUrl[@purpose='{0}']/@name", _
objectName)).Value
Return b
End If
End Function
Overloads Shared Function getBusinessFactory( _
ByVal zone As String, ByVal url As String) _
As general.dkIBusinessFactory
Dim fact1 As dkcs.business.general.dkIBusinessFactory
If url = "" Then
fact1 = New dkBusinessFactoryConfigurable()
Else
Dim args() As Object
Dim activationAttributes() = {New UrlAttribute(url)}
Dim hdlSample As ObjectHandle
"dkcs.business.factory.dkBusinessFactoryConfigurable", _
True, BindingFlags.Instance Or BindingFlags.Public, Nothing, _
args, Nothing, activationAttributes, Nothing)
hdlSample = Activator.CreateInstance("businessFactory", _
"dkcs.business.factory.dkBusinessFactoryConfigurable", _
True, BindingFlags.Instance Or BindingFlags.Public, Nothing, _
args, Nothing, activationAttributes, Nothing)
fact1 = CType(hdlSample.Unwrap(), dkBusinessFactoryConfigurable)
fact1.zone = zone
End If
fact1.zone = zone
Return fact1
End Function
End Class
Public Class dkBusinessFactoryHost
Inherits dkBusinessFactoryConfigurable
Dim WithEvents hostedObject As dkIBusinessObject
Public Sub New(ByVal myobject As dkIBusinessObject)
Me.myobject = myobject
End Sub
Public Sub doit(ByVal ds As DataSet)
domessagearrival(ds)
End Sub
Private Sub hostedObject_changeDataset(ByVal ds As _
System.Data.DataSet) Handles hostedObject.changeDataset
domessagearrival(ds)
End Sub
End Class
<Serializable()> _
Public Class dkBusinessFactoryConfigurable
Inherits dkcs.general.general.nonServicedconfigurator
Implements dkIBusinessFactory
Dim mvarname As String
Dim mvarZone As String
Protected WithEvents myobject As _
dkcs.business.general.dkIBusinessObject
Public Sub New()
MyBase.new()
End Sub
Public Property zone() As String _
Implements dkIBusinessFactory.zone
Get
Return mvarZone
End Get
Set(ByVal Value As String)
mvarZone = Value
Me.configuration = dkZoneConfiguration.zoneConfiguration( _
Value).SelectSingleNode("businessObjects")
End Set
End Property
Public Property name() As String Implements _
dkIBusinessFactory.name
Get
Return (mvarname)
End Get
Set(ByVal Value As String)
mvarname = Value
If Value <> "" Then
myobject = getBusinessObject(name, New DataSet())
myobject.businessFactory = Me
Me.listenToBroadcaster(myobject)
End If
End Set
End Property
Public ReadOnly Property businessobject() As _
dkcs.business.general.dkIBusinessObject Implements _
dkIBusinessFactory.businessObject
Get
Return myobject
End Get
End Property
Public Function blankdataset() As System.Data.DataSet _
Implements dkcs.business.general.dkIBusinessFactory.blankdataset
Return myobject.blankdataset
End Function
Public Overloads Function getDataSet() As System.Data.DataSet _
Implements dkcs.business.general.dkIBusinessFactory.getDataSet
Return myobject.getDataSet
End Function
Public Overloads Function setDataSet(ByVal ds As _
System.Data.DataSet) As System.Data.DataSet Implements _
dkcs.business.general.dkIBusinessFactory.setDataSet
Return myobject.setDataSet(ds)
End Function
Public ReadOnly Property key() As Object Implements _
dkcs.business.general.dkIBusinessFactory.key
Get
Return name
End Get
End Property
Public Property initDataset() As System.Data.DataSet _
Implements dkcs.business.general.dkIBusinessFactory.initDataset
Get
Return myobject.initDataset
End Get
Set(ByVal Value As System.Data.DataSet)
myobject.initDataset = Value
End Set
End Property
Public Event changeDataset(ByVal ds As System.Data.DataSet) _
Implements dkcs.business.general.dkIBusinessFactory.changeDataset
Public Property businessFactory() As _
dkcs.business.general.dkIBusinessFactory Implements _
dkcs.business.general.dkIBusinessFactory.businessFactory
Get
Return Me
End Get
Set(ByVal Value As dkcs.business.general.dkIBusinessFactory)
Throw New Exception( _
"cannot set a business factory's businessfactory")
End Set
End Property
Public Overloads Function getDataSet(ByVal parentDataSet As _
System.Data.DataSet, ByVal tableName As String) As _
System.Data.DataSet Implements _
dkcs.business.general.dkIBusinessFactory.getDataSet
Dim ds As DataSet = myobject.getDataSet(parentDataSet, tableName)
Return ds
End Function
Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
If disposing Then myobject.Dispose()
MyBase.Dispose(disposing)
End Sub
Public Property initDataSetString() As String Implements _
dkcs.business.general.dkIBusinessFactory.initDataSetString
Get
Return myobject.initDataSetString
End Get
Set(ByVal Value As String)
myobject.initDataSetString = Value
End Set
End Property
Public Sub storedataset() Implements _
dkcs.business.general.dkIBusinessFactory.storedataset
myobject.storedataset()
End Sub
Public Function getBusinessObject(ByVal name As String, _
ByVal parentDataSet As System.Data.DataSet) As _
dkcs.business.general.dkIBusinessObject Implements _
dkcs.business.general.dkIBusinessFactory.getBusinessObject
If Not configuration.SelectSingleNode(name) Is Nothing Then
Dim n As Xml.XmlElement = _
dkcs.general.general.xmlMerge.externalCheck( _
configuration.SelectSingleNode(name))
Dim bo As dkcs.business.general.dkIBusinessObject = _
dkcs.general.general.objectFactory.getConfiguredObject( _
configuration.SelectSingleNode(name).SelectSingleNode( _
"businessObject"))
Return bo
End If
End Function
Public Overloads Function setDataSet(ByVal ds As _
System.Data.DataSet, ByVal objectName As String) As _
System.Data.DataSet Implements _
dkcs.business.general.dkIBusinessFactory.setDataSet
Return myobject.setDataSet(ds, objectName)
End Function
Public Overloads Function command(ByVal ParamArray commandstring() _
As String) As String Implements _
dkcs.business.general.dkIBusinessFactory.command
Return Me.businessobject.command(commandstring)
End Function
Public Function setBusinessObject(ByVal name As String, _
ByVal businessObject As dkcs.business.general.dkIBusinessObject) _
As dkIBusinessObject Implements _
dkcs.business.general.dkIBusinessFactory.setBusinessObject
Return Me.businessobject.setBusinessObject(name, businessObject)
End Function
End Class
Presentation Tier
The Presentation Tier is a set of classes written with the .NET Framework.
The objects within the presentation tier form a complex hierarchy depending on the presentation tier's
configuration and the structure of the business tier. Presentation objects are capable of holding references
to business objects in the business tier and of requesting 'sub objects' from those business tier objects
and generating nested objects that hold those business tier objects.
When events are raised by the business tier objects they are received by the presentation tier objects
which then update their content in response to the new data received.
The presentation tier objects are capable of requesting functions from the business tier objects by sending them datasets.
Presentation Tier objects are capable of having a number of 'behaviours'. A behaviour controls how
the presentation object actually renders itself to the user. This makes the presentation object itself
independent from the presentation technology.
Currently the 'behaviour' used holds a reference to a DHTML object within the object model of MSHTML.
MSHTML is the COM control that Internet Explorer uses to render HTML content to the user. Each DHTML
control is an element within the HTML rendered by MSHTML. The presentation control directly manipulates
the properties of its hosted DHTML control and responds to the events that it raises. This makes for a
'live' page with different parts responding to different business objects at different network and process locations.
When the presentation control itself contains other presentation controls the DHTML controls that
these contained presentation controls host are themselves contained in the parent presentation object's
hosted DHTML control. This architecture differs from a web page which, generally connects to a single server,
requires the entire page to be refreshed when it is updated and does not respond to events raised by the server.
The presentation tier is configured in a set of XML files. Each presentation object is configured in these files.
The files configure the name and location of the root object for the display and then a hierarchy of controls.
Providing it is connecting to a business tier that implements the standard business tier interfaces the
same presentation can be configured to connect to entirely different business tiers with different functions.
The XML file for the presentation tier configures the type of a number of different presentation
tier controls and also the type of its behaviours. Then within the hierarchy of presentation tier controls
it configures for each presentation control:
- How to obtain its business object (if it obtains a new one)
- An XPath to the data from the business object to display
- Attributes for its hosted object (for DHTML objects this would be HTML attributes (class etc)
- Presentation controls that are contained within the control
- Presentation controls that are generated when the control is expanded.
- Menu options available by right clicking on an element
Elements within the presentation configuration can refer to external files whose content is merged with theirs.
Presentation Tier configuration example
This XML (and the external files it references)
configures how confirmations are displayed:
<listedControl function='expander'>
<menuControls />
<containedControls>
<dkXmlAttributeControl function='label'
dkKey='authorisationLabel' attributeName="@confirmSet">
<configurator>
<oneOffs>
<htmlAttributes>
<class>authorisation</class>
<style>width:20%</style>
</htmlAttributes>
</oneOffs>
</configurator>
</dkXmlAttributeControl>
</containedControls>
<subControls>
<listingControl function='list'
xpath="confirmations:confirmation"
keyAttribute="guid" childIndex='99'>
<listedControl function='expander'>
<configurator>
<oneOffs>
<expanded>true</expanded>
</oneOffs>
</configurator>
<containedControls>
<dkXmlAttributeControl function='label'
attributeName="@confirmType" dkKey='confirmType'>
<configurator>
<oneOffs>
<htmlAttributes>
<width>10%</width>
<class>confirmation</class>
</htmlAttributes>
</oneOffs>
</configurator>
</dkXmlAttributeControl>
</containedControls>
<subControls>
<listingControl function='list'
xpath="confirmations:confirmationAttempt[@confirmationResult]/.."
keyAttribute="" childIndex='1' dkKey='failedAttempts'>
<listedControl function='expander'>
<configurator>
<oneOffs></oneOffs>
</configurator>
<containedControls>
<dkXmlAttributeControl function='label' dkKey='error'>
<configurator>
<oneOffs>
<prefix>failed Attempts</prefix>
<htmlAttributes>
<class>error</class>
</htmlAttributes>
</oneOffs>
</configurator>
</dkXmlAttributeControl>
</containedControls>
<subControls>
<listingControl function='list'
xpath="confirmations:confirmationAttempt[@confirmationResult]"
keyAttribute="guid">
<listedControl function='expander'
externalFilePath='confirmationAttempt.xml' />
</listingControl>
</subControls>
</listedControl>
</listingControl>
<listingControl function='list'
xpath="confirmations:confirmationAttempt[not (@confirmationResult)]"
keyAttribute="guid"
externalFilePath='confirmationAttempt.xml' dkKey='confirmationAttempt'>
<listedControl function='expander'
externalFilePath='confirmationAttempt.xml' />
</listingControl>
</subControls>
</listedControl>
</listingControl>
</subControls>
</listedControl>
Points of Interest
I struggled a bit with the MSHTML object model and still have not resolved everything.
For example I never managed to receive keypress events from individual objects. I'm looking forward to the
new webbrowser control and wonder I Microsoft have gone so far as to sort out access to all the DHTML objects in the model.
History