Click or drag to resize
Post-processing

[This is preliminary documentation and is subject to change.]

Post-processing is used, when you want some code to be executed, whenever a user saves (or deletes) a dynamic form.

This topic contains the following sections.

Implementation

A post-process is typically implemented as a an .ASPX-file. The application developer developing the .ASPX may choose whether to use "code behind" or not.

The .ASPX-file (together with an optional "code behind" file, e.g. .ASPX.CS file) must be placed in the PostProcess folder in the configuration folder structure.

The post-process ASPX should inherit from a base page TopicaPostProcessRedirect. This gives access to the entire TOPICA framework API, plus a few utilities that makes writing post-process ASPX'es easier.

C#
<%@ Page Language="C#" AutoEventWireup="true" Inherits="TopicaPostProcessRedirect"  %>

Be warned: the TOPICA framework API evolves over time. Therefore, a post-process developed and tested against a particular version of the framework, may not work when used with a newer version of the framework.

In order to use the TOPICA framework API, some namespaces should be imported. This example imports various useful namespaces - other namespaces may be needed depending on the code that is needed in the post-process.

C#
<%@ Import Namespace="System.Diagnostics" %>
<%@ Import Namespace="System.Data.Common"  %>
<%@ Import Namespace="CSC.SC.Enterprise.DataAccess" %>
<%@ Import Namespace="CSC.SC.TOPICA4.Library" %>
<%@ Import Namespace="CSC.SC.TOPICA4.DynamicData"  %>
<%@ Import Namespace="CSC.SC.TOPICA4.Controllers"  %>

A post-process routine may or may not display user interface.

  • Normally a post-process does NOT display a user interface - it runs "silently" whenever the user saves or deletes a record. If the post-process routine does not display user interface, it should automatically continue with the normal flow in the framework.

  • A post-process routine may display some user interface, either as a general rule, in case of errors, or as a debugging aid. If the post-process routine displays user interface, the user must have a chance to see it. Therefore, the post-process routine should not continue with the normal flow in the framework. Instead, the post-process webform must display a hyperlink (or equivalent), that the user may click on to continue with the normal flow in the framework.

In any case, it is the responsibility of the application developer to make sure, that the post-process continues with the appropriate normal flow of control. The base page contains some utilities, for examle method Return(Panel, HyperLink), to help the developer with this.

Normally you would write the code in post-process ASPX in the Page_Load event. But you may use any event defined by ASP.NET's "page life cycle".

The base page also contains a few tools that may be used for debugging (methods ShowDebug(string) and ShowDebugRecord(string, Record)). Debugging (which is per default off) is turned on by specifying

<add key="DynamicForm.PostProcess.Debug" value="true" />

in the Server.config file (refer to .config files in the Configuration Guide).

When debugging is turned on, calls to ShowDebug(string) and ShowDebugRecord(string, Record) produce debug output, which is written to the browser (using Response.Write). This is a useful tool for debugging post-process ASPX'es. Leave these calls in the code when deployering to production environment. When debugging is off, they do no harm. If an error occurs in production, it is easy to turn debug output on - this may help identify problems.

Typically, the application developer would use Visual Studio to develop and debug post-process-ASPX'es. In order to get intellisense, the TOPICA framework and the post-process being developed should be added to the same solution.

Configuration

The configurator must write TOPICA Basic code to compute the URL referring to the post-process .ASPX.

Configuration/{configurationname}/PostProcess/MyPostProcess.aspx?{CommonParameters}&patient=id:{patient.id}&record={record.tablename}:{record.id}

It is only possible to define 1 post-process .ASPX on each form - and it MUST be defined on the Form template item (the topmost item).

  • Remember to specify the entire relative path to the post-process ASPX (relative to the framework folder).

    This part of the expression Configuration/{configurationname} is how you refer to the configuration folder using TOPICA Basic - this will be the same for all post-processes.

    In theory it is possible to point anywhere - even outside the framework. But this would not be very useful in real-life situations.

  • The query string (part of the URL after '?') specifies context (current patient and current record). This will be the same for all post-processes.

    This part of the expression: {CommonParameters} is how you pass various common parameters needed for practically every page in TOPICA. These parameters contain the current configuration, the session-GUID (identifying the session), and variaous other parameters depending on the circumstances. When using the notation {CommonParameters}, the configurator does not have to know the details of passing common parameters around.

  • Caution note Caution

    It currently is possible to define post-process .ASPX on template items other than Form - but these will NOT work!

Examples

The below samples are part of the Demo application

Example: updating patient properties from dynamic form

The Patient object is static (not configurable). Consquently, the framework contains a built-in, fixed property form used for editing patient properties. This form contains a fixed set of fields, that are typically used in clinical applications (name, address, death date, etc.).

You might want to be able to edit some of these properties from a dynamic (configured) form. For example, patients might die during an admission to a hospital. Suppose the application contains a discharge form. It would be logical, that the user had the possibility to enter the death date directly in the discharge form, instead of forcing the user to save the discharge form, and then open the patient property form to edit the death date.

It is no problem to add a death date field to the discharge form. However, in order to keep any reports etc. that read death date from the patient properties (i.e. the Patient database table) working, it is important, at the death date in the patient properties is set, whenever a death date is entered in the discharge form.

The solution: use a post-process routine.

In the form PatientUpdater in the Demo application, the property PostProcess on the Form element is set to this value:

C#
<%@ Page Language="C#" AutoEventWireup="true" Inherits="TopicaPostProcessRedirect"  %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<%@ Import Namespace="System.Diagnostics" %>
<%@ Import Namespace="System.Collections.Generic" %>
<%@ Import Namespace="System.Data.Common"  %>
<%@ Import Namespace="CSC.SC.Enterprise.DataAccess" %>
<%@ Import Namespace="CSC.SC.TOPICA4.Library" %>
<%@ Import Namespace="CSC.SC.TOPICA4.DynamicData"  %>
<%@ Import Namespace="CSC.SC.TOPICA4.Controllers"  %>

<script runat="server">

    // This aspx "PatientUpdater.aspx" is configured as post process  for the PatientUpdater form
    // with the following TOPICAbasic statement
    // "Configuration/{configurationname}/PostProcess/PatientUpdater.aspx?{CommonParameters}&patient=id:{patient.id}&record={record.tablename}:{record.id}"

    protected void Page_Load()
    {
        // ObjectContext.Record returns the record in context (specified in the querystring)
        if (ObjectContext.Record == null)
        {
            // No context record - internal error. Display error and exit.
            base.ShowDebug("No record");
            PanelErrorNoRecord.Visible = true;
        }
        else
        {
            // The FooBarCreator record exists. Update subrecords
            base.ShowDebugRecord("Record in context: ", base.ObjectContext.Record);
            // 
            // Update the patient.
            UpdatePatient();
            // 
            // The FooBarCreatorPostProcess.aspx page inherits from TopicaPostProcessRedirect
            // it uses the Return method to return to the correct page when done
            base.Return(this.PanelContinue, this.HyperLinkContinue);            
        }
    }

    private void UpdatePatient()
    {
        base.ShowDebug("Updating patient: " + base.ObjectContext.Patient.ToString());
        base.ShowDebug(".. old death date value: " + base.DateTimeConverter.ToStringDateTime(base.ObjectContext.Patient.DeathDate));
        base.ObjectContext.Patient.DeathDate = DataRowUtil.GetDateTime2(ObjectContext.Record.DataRow, "DeathDate");
        ShowDebug(".. new death date value: " + base.DateTimeConverter.ToStringDateTime(base.ObjectContext.Patient.DeathDate));
        var renewConset = DataRowUtil.GetBool(ObjectContext.Record.DataRow, "RenewConsent");
        if (renewConset)
        {
            var orgUnit = ObjectContext.Record.DBGetOrgUnit(Database);
            DateTime? oldConsentDate = base.ObjectContext.Patient.ConsentDate;
            base.ShowDebug(".. old consent date value: " + base.DateTimeConverter.ToStringDateTime(oldConsentDate));
            DateTime? newConsentDate = DateTime.Now;
            base.ObjectContext.Patient.ConsentDate = newConsentDate;
            base.ObjectContext.Record.DataRow["RenewConsent"] = false;
            base.RecordController.Update(ObjectContext.Record, orgUnit, FormContext); // clear the "renew" flag
            base.ShowDebug(".. new consent value: " + base.DateTimeConverter.ToStringDateTime(newConsentDate));
        }
        base.PatientController.Update(base.ObjectContext.Patient);
    }

</script>

<html xmlns="http://www.w3.org/1999/xhtml" >
<head id="Head1" runat="server">
    <title>{WindowTitle} - PatientUpdater post-process</title>
    <link rel="stylesheet" href="../../../StyleSheets/Screen.css" media="screen" type="text/css" />
    <link rel="stylesheet" href="../../../StyleSheets/Print.css" media="print" type="text/css" />
    <base target="_self"/>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <h3>FooBarCreator post-process</h3>
        <asp:Panel ID="PanelErrorNoRecord" runat="server" CssClass="ErrorMessage" Visible="false">
            No context record (internal error)
        </asp:Panel>
        <asp:Panel ID="PanelContinue" runat="server" Visible="false">
            <asp:HyperLink ID="HyperLinkContinue" runat="server">Continue...</asp:HyperLink>
        </asp:Panel>        
    </div>
    </form>
</body>
</html>
Configuration/{configurationname}/PostProcess/PatientUpdater.aspx?{CommonParameters}&patient=id:{patient.id}&record={record.tablename}:{record.id}
Example: shared fields

Suppose there are some fields, that you want to be "shared" between 2 or more forms. It is not possible to specify "shared fields" in the form templates, because all fields defined in a form are stored in one table (except ArrayTable, but this is besides the point).

But you may obtain the required functionality using a combination of computed default values and post-processing. The following example is part of the Demo application:

  • 2 forms (SharedForm1 and SharedForm2) have a number of fields (SharedDate, SharedInteger, SharedTextArea) in common.

  • These 2 forms (SharedForm1 and SharedForm2) are defined as sibling forms (having common parent = form PrePostProcessContainer), and relations between the forms are set to 1:1 cardinality. This means, that there will always be 0 or 1 instance of both SharedForm1 and SharedForm2.

  • The Computation properties on the shared fields (SharedDate, SharedInteger, SharedTextArea) in both forms are defined to get data from the corresponding field in the other form (if the form is created).

  • The above steps insure, that default values for the shared fields are read from the other form, if it has been created.

  • The property PostProcess on both forms are set to this value:

    Configuration/{configurationname}/PostProcess/SharedForm.aspx?{CommonParameters}&patient=id:{patient.id}&record={record.tablename}:{record.id}

    Note how the path points to the PostProcess folder, and how the context (current patient and current record) is specified in the querystring.

    In this demo, the same post-process-routine is used for both forms. This means that the post-process must check which form was in context, because it should update the OTHER form. It would have been possible to use different ASPX'es for the two forms - but most of the code would be the same.

The post-process SharedForm.aspx looks like this:

C#
<%@ Page Language="C#" AutoEventWireup="true" Inherits="TopicaPostProcessRedirect"  %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<%@ Import Namespace="System.Diagnostics" %>
<%@ Import Namespace="System.Data.Common"  %>
<%@ Import Namespace="CSC.SC.Enterprise.DataAccess" %>
<%@ Import Namespace="CSC.SC.TOPICA4.Library" %>
<%@ Import Namespace="CSC.SC.TOPICA4.DynamicData"  %>
<%@ Import Namespace="CSC.SC.TOPICA4.Controllers"  %>

<script runat="server">

    // This aspx "SharedForm.aspx" is designed as a post-process routine for forms SharedForm1 and SharedForm2
    // Specify this parameter
    // "Configuration/{configurationname}/PostProcess/SharedForm.aspx?{CommonParameters}&patient=id:{patient.id}&record={record.tablename}:{record.id}"
    // in the PostProcess attribute

    protected void Page_Load()
    {
        // base.ObjectContext.Record holds record in context
        if (base.ObjectContext.Record == null)
        {
            // No record in context - should never happen
            ShowDebug("No record");
            PanelErrorNoRecord.Visible = true;
            return; // STOP - interrupt normal flow of control!
        }
        // Context record exists
        base.ShowDebugRecord("Record in context: ", base.ObjectContext.Record);
        string contextTableName = base.ObjectContext.Record.TableName;
        string updateTableName;
        switch (contextTableName.ToLower())
        {
            case "sharedform1":
                updateTableName = "SharedForm2";
                break;
            case "sharedform2":
                updateTableName = "SharedForm1";
                break;
            default:
                this.PanelErrorUnknownTableName.Visible = true;
                this.LabelErrorUnknownTableName.Text = contextTableName;
                return; // STOP - interrupt normal flow of control!
        }
        DataDictionaryTable updateTable = base.DataDictionary.FindTable(updateTableName);
        Record parentRecord = base.ObjectContext.Record.DBGetParentRecord(Database, DataDictionary);
        RecordCollection updateRecordCollection = parentRecord.DBGetSubrecords(base.Database, updateTable);
        switch (updateRecordCollection.Count)
        {
            case 0:
                base.ShowDebug("No record found with table name " + updateTableName);
                break;
            case 1:
                Record updateRecord = updateRecordCollection.First();
                base.ShowDebugRecord("Updating record with shared fields: ", updateRecord);
                updateRecord.DataRow["SharedDate"] = base.ObjectContext.Record.DataRow["SharedDate"];
                updateRecord.DataRow["SharedInteger"] = base.ObjectContext.Record.DataRow["SharedInteger"];
                updateRecord.DataRow["SharedTextArea"] = base.ObjectContext.Record.DataRow["SharedTextArea"];
                OrgUnit orgUnit = base.ObjectContext.Record.DBGetOrgUnit(base.Database);
                base.RecordController.Update(updateRecord, orgUnit, base.FormContext);
                break;
            default:
                base.ShowDebug("Found " + updateRecordCollection.Count.ToString() + " records with table name " + updateTableName + " - internal error");
                this.LabelErrorRecordCount.Text = updateRecordCollection.Count.ToString();
                this.PanelErrorNotUnique.Visible = true;
                return;    // STOP - interrupt normal flow of control!
        }
        // 
        // Since this postprocess page inherits from TopicaPostProcessRedirect
        // it can (should!) use the inherited Return method to return to the correct page when done
        base.Return(this.PanelContinue, this.HyperLinkContinue);            
    }


</script>

<html xmlns="http://www.w3.org/1999/xhtml" >
    <head id="Head1" runat="server">
        <title>{WindowTitle} - SharedForm post-process</title>
        <link rel="stylesheet" href="../../../StyleSheets/Screen.css" media="screen" type="text/css" />
        <link rel="stylesheet" href="../../../StyleSheets/Print.css" media="print" type="text/css" />
        <base target="_self"/>
    </head>
    <body>
        <form id="form1" runat="server">
        <div>
            <h3>SharedForm post-process</h3>
            <asp:Panel ID="PanelErrorNoRecord" runat="server" CssClass="ErrorMessage" Visible="false">
                No record specified
            </asp:Panel>
            <asp:Panel ID="PanelErrorNotUnique" runat="server" CssClass="ErrorMessage" Visible="false">
                <asp:Label ID="LabelErrorRecordCount" runat="server"></asp:Label> records found - 0 or 1 expected!
            </asp:Panel>
            <asp:Panel ID="PanelErrorUnknownTableName" runat="server" CssClass="ErrorMessage" Visible="false">
                Unkown table name: <asp:Label ID="LabelErrorUnknownTableName" runat="server"></asp:Label>
            </asp:Panel>
            <asp:Panel ID="PanelContinue" runat="server" Visible="false">
                <asp:HyperLink ID="HyperLinkContinue" runat="server">Click here to continue normal flow of control...</asp:HyperLink>
            </asp:Panel>
        </div>
        </form>
    </body>
</html>

This is a fully working example (including error handling) that may be tried out in the Demo application.

Note the use of ShowDebug and ShowDebugRecord for debugging.

There are several ways (apparently equally valid) ways to obtain the same result. For example, the Record class has a method DBUpdate(). In the above code, this method is not used (directory). Updating a record is done by base.RecordController.Update. The reason for this is, that calling through the controlelr layer handles logging:

  • Calling base.RecordController.Update writes an entry in the transaction log.

  • Calling Record.DBUpdate would NOT write an entry in the transaction log.

Example: create/update/delete record

You may want to implement automatic creation of records, depending on user input in the current form. Example: the form is used to enter data about examination results. There could be a number of possible consequences: treatment A, treamtne B or no treatment. The user selects the desired consequence (e.g. using radiobuttons), and the post-process routine will create records corresponding to treatment A, treatment B, or no records. The post-process routine may copy data (e.g. regarding referral, anamnesis, indication) from the examintation to the new treatment records - saving user from entering the same data again.

The demo application contains some forms (FooBarCreateForm, FooForm, BarForm), that demonstrate how to create, update and delete records using post-process code, control by fields entered by the user.

C#
<%@ Page Language="C#" AutoEventWireup="true" Inherits="TopicaPostProcessRedirect"  %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<%@ Import Namespace="System.Diagnostics" %>
<%@ Import Namespace="System.Collections.Generic" %>
<%@ Import Namespace="System.Data.Common"  %>
<%@ Import Namespace="CSC.SC.Enterprise.DataAccess" %>
<%@ Import Namespace="CSC.SC.TOPICA4.Library" %>
<%@ Import Namespace="CSC.SC.TOPICA4.DynamicData"  %>
<%@ Import Namespace="CSC.SC.TOPICA4.Controllers"  %>

<script runat="server">

    // This aspx "FooBarCreatePostProcess.aspx" is configured as post process  for the FooBarCreator form
    // with the following TOPICAbasic statement
    // "Configuration/{configurationname}/PostProcess/FooBarCreatorPostProcess.aspx?{CommonParameters}&patient=id:{patient.id}&record={record.tablename}:{record.id}"

    protected void Page_Load()
    {
        // ObjectContext.Record returns the record in context (specified in the querystring)
        if (ObjectContext.Record == null)
        {
            // No context record - internal error. Display error and exit.
            ShowDebug("No record");
            PanelErrorNoRecord.Visible = true;
        }
        else
        {
            // The FooBarCreator record exists. Update subrecords
            base.ShowDebugRecord("Record in context: ", base.ObjectContext.Record);
            UpdateSubrecord("Foo", "CreateFooCheck");
            UpdateSubrecord("Bar", "CreateBarCheck");
            // 
            // The FooBarCreatorPostProcess.aspx page inherits from TopicaPostProcessRedirect
            // it uses the Return method to return to the correct page when done
            Return(this.PanelContinue, this.HyperLinkContinue);            
        }
    }

    /// <summary>
    /// Crate/update/delete subform
    /// </summary>
    /// <param name="dataDictionaryTableName">Name of the DataDictionarytable for subrecord that should be created/updated/deleted</param>
    /// <param name="checkFieldname">Name of field in record that decides whether record should exist</param>
    private void UpdateSubrecord(string dataDictionaryTableName, string checkFieldname)
    {
        // Fetch parent record (FooBarCreator)
        var parent = base.ObjectContext.Record.DBGetParentRecord(Database, DataDictionary);
        // 
        // Fetch OrgUnit from parent record so it can be used in the sub record
        // NB: assumes that none of the tables have relation to OrgUnit (= data owner)
        // or that the subfomrs inherit parents OrgUnit (= data owner)
        var orgUnit = parent.DBGetOrgUnit(Database);
        // 
        // The DataDictionary.FindTable method is used for finding DataDictionaryTable object by name
        var table = base.DataDictionary.FindTable(dataDictionaryTableName);
        // 
        // DBGetSubrecords fetches all records of the given dataDictionaryTable 
        // type (so we can check if the record was already created)
        // Both the Foo and the Bar form is defined with a 1:1 relation to their 
        // parent record, but there is no automatic check in the TOPICA framework
        // that prevents the developer from creating 1:n , it is up to the post 
        // process developer to prevent this
        var recordCollection = parent.DBGetSubrecords(Database, table);
        // 
        // Check parent record if a sub record should be exist
        var createRecord = DataRowUtil.GetBool(ObjectContext.Record.DataRow, checkFieldname);
        if (createRecord)
        {
            if (recordCollection.Count == 0)
            {
                // subrecord does not exist - create it
                var autoCreatedRecords = new RecordCollection();
                var errorCodes = new StringCollection();
                var dynamicRecord = new DynamicRecord(table);
                dynamicRecord.DataRow["StartDate"] = DateTime.Now;
                RecordController.Create(parent, orgUnit, FormContext, dynamicRecord, false, false, true, autoCreatedRecords, errorCodes);
            }
            else
            {
                // subrecord exists - update it
                var record = recordCollection[0];
                record.DataRow["StartDate"] = DateTime.Now;
                base.RecordController.Update(record, orgUnit, FormContext);
            }
        }
        else
        {
            if (recordCollection.Count > 0)
            {
                // one (or more?) subrecords exist - delete first one
                base.RecordController.Delete(recordCollection[0]);
            }
        }
    }

</script>

<html xmlns="http://www.w3.org/1999/xhtml" >
    <head id="Head1" runat="server">
        <title>{WindowTitle} - FooBarCreator post-process</title>
        <link rel="stylesheet" href="../../../StyleSheets/Screen.css" media="screen" type="text/css" />
        <link rel="stylesheet" href="../../../StyleSheets/Print.css" media="print" type="text/css" />
        <base target="_self"/>
    </head>
    <body>
        <form id="form1" runat="server">
            <div>
                <h3>FooBarCreator post-process</h3>
                <asp:Panel ID="PanelErrorNoRecord" runat="server" CssClass="ErrorMessage" Visible="false">
                    No context record (internal error).
                </asp:Panel>
                <asp:Panel ID="PanelContinue" runat="server" Visible="false">
                    <asp:HyperLink ID="HyperLinkContinue" runat="server">Continue...</asp:HyperLink>
                </asp:Panel>        
            </div>
        </form>
    </body>
</html>

Again, note that CRUD-operations are invoked on the controller level - to enable logging.