Faq

Contents

What is OABS?

Oabs is a new database driven online portal designer for QFlow. Being data driven - it means that all online pages have a common codebase, and can more easily be supported, maintained and upgraded. Oabs stands for 'Online Appointment Booking System' - although, technically, it is not limited to 'booking appointments' - you can enqueue appointments (e-tickets), check tickets, and many other activities.

What are the components of OABS?

Oabs consists of back end database configuration, a configurator app (this app), and a front end app. The front end app can act both as a website, and as an API.

How do I create a site?

Firstly - create a 'Journey'. A journey has a unique Id (a guid), and can then be configured to contain a series of pages (steps). On each step, you can add page widgets (step controls), such as a list, or an image, or text. You can set each Step Control to have a data source, such as pulling a list of Appointment Types, or Dates and Times.

Each 'Step' can be of a different type, either a normal 'Step' which means you will click 'next' to proceed to the next page, or an 'Action' or 'End'. An action step will execute the booking/enqueueing/cancalltion/rescheduling etc of the booking, and the end step signals that there are no further pages, a 'confirmation' page per se.

Once a journey is created - you can go to the corresponding front end OabsWeb site and load the journey by going to http://[OabsWeb]/?Id={JourneyId} or http://[OabsWeb]/appointment/index/{JourneyId}

How can I load images, css, js fonts etc?

Images can be loaded in several ways. Firstly - you can reference eternal images directly - if the image is publically avaiable via a URL, just use the URL. Secondly, you can upload images into QFlow Storage. OABS will download content from QFlow storage whenever it starts, and this content can then be locally referenced. The content must be uploaded to the OABSEngine/AdditionalAssemblies/[JourneyExtRef] folder

The [JourneyExtRef] is set in the configurator Journey settings:

You can now reference these images in Oabs by using the shortcuts:
~/{journey}/[imagename] - i.e., ~/{journey}/test.png
Oabs will replace this dynamically with http://[OabsWeb]/Tempscriptcompile/qflowweb/oabsengine/additionalassembies/pah/test.png

Lastly, though not recommended - items can be added directly into the OABS folder structure - but keep in mind this requires physical file deployment, and may be lost after an upgrade

What do ~ and {journey} do?

Oabs will automatically replace the ~ character with the current base url of OABS, i.e., the http/s address of the server and oabs directory, i.e.:
http/s://[oabsserver]/[oabsweb]

It will replace {journey} with the AdditionalAssemblies and Journey ExtRef set in the Journey, i.e. /Tempscriptcompile/qflowweb/oabsengine/additionalassembies/pah/

How can I execute OABS script?

If the JourneyExtRef is set, Oabs will automatically look for a script the with same ExtRef and execute it.

How can I hide the 'MiniProfiler' stats and 'BETA/DEBUG mode' text?

You can simply change the web.config 'debug' setting from true to false

<compilation debug="false" targetFramework="4.7.2" />

What is the lifecycle of a StepControl?

When a Step (a page) is to be rendered, OABS will loop though each of the steps 'StepControls', and render them.

To do this, it will undertake several action - and several scripts, as shown in the following diagram

Here is the overview lifecycle:

  • Foreach StepControl
  • GetParameters()
  • Script: BeforePopulateDisplayData
  • GetData()
  • AfterPopulateDisplayData

First, the StepControl will call a internal method called 'GetParameters'. This will load the list of parameters from the DataParameters table - populating them with either the hardcoded values (i.e. UnitId=5 below) or from JourneyData parameters

It will then call the script event 'BeforePopulateDisplayData, passing the Journey, Step, Model. This can be used to alter the set Parameters, or even prevent the StepControl from calling the database

Then, if allowed, it will call 'GetData()' which will call the database with the provided parameters

Finally, it will call AfterPopulateDisplayData, where the data can be altered before being rendered in the client

How can I remove results from a DataControl results?

For this, you will use the 'AfterPopulateDisplayData' script event. This is called for each StepControl that is on the page (Step) after it retrieve data from the database. You can then remove items from the results.

Firstly, you need to know which Model type the StepControl is using. You can get this from OABSConfig

In this instance - it is a QFlowObjectModel

Now, in our script, we add a condition in, so that we are picking up when the particular StepControl is running, in this case, the #AppointmentTypeId

        
            public static ScriptResults AfterPopulateDisplayData(JourneyData journeyData, AStepControl stepControl, ref AModel model)
            {
                if (stepControl.FieldName == "AppointmentTypeId")
                {
                }

                return new ScriptResults();
            }
        
        
Now, we need to cast the 'model' as the Type, after which we can access it's DisplayData property, and alter the rendered data.
        
                (model as OABSEngine.Models.QFlowObjectModel).DisplayData
            
        
Now finally - there are several different ways we can remove items from the list of items, some direct, some using built-in helpers
        
                public static ScriptResults AfterPopulateDisplayData(JourneyData journeyData, AStepControl stepControl, ref AModel model)
                {
                    if (stepControl.FieldName == "AppointmentTypeId")
                    {
                        //remove AppointmentType ID == 9
                        (model as OABSEngine.Models.QFlowObjectModel).DisplayData.RemoveAll(dd => dd.Id == 9);

                        //use shortcut method 'RemoveIfPropertyNotEqual' (or RemoveIfPropertyEqual)
                        (model as OABSEngine.Models.QFlowObjectModel).DisplayData.RemoveIfPropertyNotEqual("ExtRef", "KEEPME", true);

                        //Remove item if the rendered text on screen contains string (or RemoveIfDisplayDoesNotContain)
                        (model as OABSEngine.Models.QFlowObjectModel).DisplayData.RemoveIfDisplayDoesContain("IGNOREME");

                        //remove any items that have the ExtRef of "REMOVEME" by direcetly accessing properties in 'ContentIems'
                        (model as OABSEngine.Models.QFlowObjectModel).DisplayData.RemoveAll(dd => dd.ContentItems.Any(CI => CI.ParameterName == "ExtRef" && CI.Value == "REMOVEME"));

                        //loop through items and remove via some conditional logic
                        foreach (var DD in (model as OABSEngine.Models.QFlowObjectModel).DisplayData.ToList())
                        {
                            //If condition == true -> (model as OABSEngine.Models.QFlowObjectModel).DisplayData.Remove(DD)
                        }
                    }

                return new ScriptResults();
                }
            
        

How do I use data from the QueryString when OABS first loads?

When loading a journey - OABS will call a script event called PreloadJourneyData.

OABS passes this script both the entire, preloaded journey (from the JourneyId), and a list of query string parameters

In combination - this can be be used to do things, such as the following examples:

  • Set a JourneyData item from a querystring value - forexample, if a &UnitExt={X} is provided in the query stirng, we can set JourneyData Item with either the UnitExtRef, or even find the UnitId and set it.
  • Prepopulate a StepControl Model Value with an item from Querystring, i.e, &CustomerId={X} to search QFlow for that customer, and set it in the #Booker StepControl model
  • Show/hide particular Steps or StepControls depending on Querystring values
  • Filter out particular Units, AppointmentTypes, Dates, Times, depending on values in a Querystring
  • Call some 'Validation' methods from data in a querystring, and show an 'Access Denied' or other page if the values aren't valid
  • Pass an encrypted 'blob' of data in the query string, descrypt it, and use it to alter the Journey, Data or StepControl Model values

There are many other use cases that can also be implemented - some example code is shown below


    public static ScriptResults PreloadJourneyData(NameValueCollection queryString, ref Journey journey)
    {
        ScriptResults sr = new ScriptResults();

        //Set the default language code (or any other journeyData)
        journey.JourneyData.TrySetValue("LanguageCode", "en");

        //Set the top unit site, so OABS shows only child sites
        Unit RootUnit = Unit.GetByExtRef("RootUnit").FirstOrDefault();
        if (RootUnit != null)
        {
            journey.JourneyData.TrySetValue("TopUnitId", RootUnit.Id);
            journey.JourneyData.TrySetValue("RootUnitId", RootUnit.Id);
            journey.JourneyData.TrySetValue("ParentUnitId", RootUnit.Id);
        }

        //Show an access denied page if the querystring 'UserId' is not provided / remove / alter steps
        if (!queryString.AllKeys.Contains("UserId") && !IsAllowed(queryString["UserId"]))
        {
            AStep End = journey.Steps.FirstOrDefault(S => S.StepType == StepType.End);
            End.StepTitle = "Access denied";
            End.StepControls.Clear();
            journey.Steps.Clear();
            journey.Steps.Add(End);
            return sr;
        }

        //remove a specific StepControl depending on a parameter
        if(!bool.TryParse(queryString["GroupBooking"], out bool GroupBooking))
        {
            journey.RemoveStepControlByFieldName("GroupBooking");
        }

        //set a specific stepcontrol to have a preselected value
        if (DateTime.TryParse(queryString["DefaultDate"], out DateTime DefaultDate) && DefaultDate!=DateTime.MinValue)
        {
            AStepControl DD = journey.GetStepControlByFieldName("AppointmentDate");
            (DD.Model as OABSEngine.Models.CalendarDateModel).Value = DefaultDate;
        }

        return sr;
    }

Does OABS work with QFlow modules such as QFlow Connect, or scripts etc?

OABS uses QFlow libary methods directly - so 'SetAppointment' will trigger any attached scripts in the BeforeSetAppointment/AfterSetAppointment and any associated ServiceProfile Connect events

How can i see the real error behind a 500 error screen?

By default, OABS will show a styled 500 error screen.

To turn this off in version 29 and earlier - it is required that both customErrors mode is turned off, and the Oabs/Errors/500.aspx file is renamed/deleted

To turn this off in v30, it is just required that the web.config customErrors mode is set to Off or RemoteOnly

            
                <customErrors mode="RemoteOnly" redirectMode="ResponseRewrite">
                <error statusCode="404" redirect="~/Errors/404.aspx" />
                <error statusCode="500" redirect="~/Errors/500.aspx" />
                </customErrors>
            
        

We can also use this to change the redirect to go to other ASPX page designs if required

Some errors are not handled by OABS, and are instead handled by IIS. These can be customised in the httpErrors section
            
                <httpErrors errorMode="DetailedLocalOnly">
                <remove statusCode="404" />
                <remove statusCode="500" />
                <error statusCode="404" path="/Errors/404.html" responseMode="ExecuteURL" />
                <error statusCode="500" path="/Errors/500.html" responseMode="ExecuteURL" />
                </httpErrors>
            

Finally - any errors that OABS throws can be seen in the QFlow event log

How are #Parameters used?

#Parameters are used in a similar way to their usage in QFlow. # Parameters will be substituted by actual values. The values can be resolved from several places, as follows:

  • JourneyData
  • ContentItems
  • QFlow DynamicText
  • Custom

  1. Firstly - JourneyData. As you proceed from one step to the next, the StepControl FieldName is used to store the Model value in JourneyData. For example, a StepControl for an AppointmentType dropdown, and a fieldname of #AppointmentTypeId will be stored in JourneyData under the Key 'AppointmentTypeId'

    Any time the value #AppointmentTypeId then appears, it will be substituted by the value found in JourneyData

    JourneyData can be added to manually, as has been shown in other examples.

    Additionally - any DataControls will attempt to populate their data from DataParameters, which can each be set to look at specific JourneyData values

  2. The second place they can be resolved from is ContentItems. All QFlow Content Templates and Values are cached in OABS - at all levels - Global, Units, AppointmentType, Service etc. OABS will search the QFlow content for a matching ObjectId (UnitId) and ContentTemplate name (and LanguageCode) for an Item, and use the value

  3. After all of these, OABS will attempt to resolve any remaining # parameters using the built in QFlow DynamicText.Resolve methods.

  4. Finally - using AfterPopulateDisplayData - each 'DisplayData' object can be used with simple string.Replace type methods, to replace any remaining #parameters with custom values

Future plans are also to examine custom resource RESX files for values.

Can I enable 'tracing' in OABS

Yes! OABS has a full tracing facility built in.

The full tracing facility can be enabled to a file output via the web.config via enabling a trace listener:

            

                <trace autoflush="false" indentsize="10">
                <listeners>
                <add name="myListener" type="System.Diagnostics.TextWriterTraceListener" initializeData="TextWriterOutput.log" traceOutputOptions="DateTime" />
                <remove name="Default" />
                </listeners>
                </trace>

            
        

This will start logging the entire OABS flow to the file, split by individual request IDs

            
                0f7d8|InternalAppointmentController|GetStep|Starting|JourneyId = e2f15387-8aac-4366-931c-3a6841a50925, PathType = Book, BackGuid = |0|10/08/2021 21:04:40|Info
                0f7d8|InternalAppointmentController|GetStep|Loading Journey|JourneyId = e2f15387-8aac-4366-931c-3a6841a50925|11|10/08/2021 21:04:40|Info
                0f7d8|Loader|Loader|BathPath|BasePath=/|12|10/08/2021 21:04:40|Info
                0f7d8|Loader|Loader|Loading Direct Models|Steps = 5|1177|10/08/2021 21:04:41|Info
                0f7d8|Loader|Loader|Completed replacing JS/CSS||1177|10/08/2021 21:04:41|Info
                0f7d8|Loader|Loader|Loaded Journey||1177|10/08/2021 21:04:41|Info
                0f7d8|Loader|Loader|Loading Direct Models Complete|Steps = 5|1226|10/08/2021 21:04:41|Info
                0f7d8|Loader|Loader|Loading Script|Script named 'OABSEngine' with ExtRef 'PAH' Loaded|1501|10/08/2021 21:04:41|Info
                0f7d8|Journey|PreloadJourneyData|Executing PreloadJourneyData script||1616|10/08/2021 21:04:41|Info
                0f7d8|Journey|PreloadJourneyData|Completed Preloading data for StepControl|StepControl = Booker|1616|10/08/2021 21:04:41|Info
            

Additionally - this can be read when in QFlow script, and potentially logged out to the EventLog


    public static ScriptResults AfterPopulateDisplayData(JourneyData journeyData, AStepControl stepControl, ref AModel model, ref Guid? redirectedStepId)
    {
        ScriptResults sr = new ScriptResults();

        string FullLog = OABSEngine.Helpers.Helpers.Logger.ToCSVString();
        OABSEngine.Engine.LogQFlowInfo("OABS Log", FullLog);

        return sr;
    }

The logging is pipe seperated, and the columns are as follows:

  • RequestId - each page hit generates a unique request ID
  • Classname - the class in which the log was written
  • Methodname - the method in which the log was written
  • The action being undertaken
  • The detailed description or data involved in the action
  • The number of milliseconds passed since the request began
  • Datetime of the log
  • Level of the log

Can I add/render more #Parameters, from external data or script?

Yes, there are several ways to do this. As detailed previously - #Parameters are by default found by examining JourneyData, ContentItems and QFlow DynamicText rendering

We can of course add as much extra data as we wish to Content Management, and OABS will pull this down. However - instead of cluttering content, we can undertake two seperate techniques for resolving extra #Parameters

Firstly - we can use the AfterPopulateDisplayData script event and string.Replace to replace any remaining unresolved #Parameters


    public static ScriptResults AfterPopulateDisplayData(JourneyData journeyData, AStepControl stepControl, ref AModel model, ref Guid? redirectedStepId)
    {
        ScriptResults sr = new ScriptResults();

        if (stepControl.FieldName == "AppointmentTypeId")
        {
            foreach (var DD in (model as OABSEngine.Models.QFlowObjectModel).DisplayData)
            {
                DD.DisplayText = DD.DisplayText.Replace("#CustomParam", "Here is a custom value for AppointmentTypeId" + DD.Id.ToString());
            }
        }

    return sr;
    }

This gets rendered as

Alternately - we can use BeforePopulateDisplayData, and add extra ContentItems to the DataControl before it renders - the extra parameters will be resolved. The DataControls include a new property called TemporaryContentItems. These last for only the lifetime of the Render.



    public static ScriptResults BeforePopulateDisplayData(ref JourneyData journeyData, ref AStepControl stepControl, ref AModel model, ref bool ExecuteDataControl, ref Guid? redirectedStepId)
    {
        ScriptResults sr = new ScriptResults();

        if (stepControl.FieldName=="UnitId")
        {
            model.DataControl.TemporaryContentItems.Add(new OABSEngine.Objects.ContentItem()
            { ContentObjectType = OABSEngine.Objects.ContentObjectType.Unit, ObjectId = 1, ParameterName = "CustomParameter", LanguageCode = "en", Value = "This is custom item 1" });

            model.DataControl.TemporaryContentItems.Add(new OABSEngine.Objects.ContentItem()
            { ContentObjectType = OABSEngine.Objects.ContentObjectType.Unit, ObjectId = 2, ParameterName = "CustomParameter", LanguageCode = "en", Value = "This is custom item 2" });
        }

        return sr;
    }

This results in the folowing:

Obviously - in this example - we only added content for TWO specific units - the units that do not have the custom item do not get resolved. These can be removed by the previously used DisplayText.Replace()

Can I alter the data parameters used in a datacontrol?

Firstly, you can use OABS config to set parameters. EIther you can set the parameter to read from a different JourneyData item - or you can hardcode the value

Secondly - in v30 onwards - you can use the BeforePopulateDisplayData script even to alter the DataControl parameters about to be used


    public static ScriptResults BeforePopulateDisplayData(ref JourneyData journeyData, ref AStepControl stepControl, ref AModel model, ref bool ExecuteDataControl, ref Guid? redirectedStepId)
    {
        ScriptResults sr = new ScriptResults();

        if (stepControl.FieldName == "AppointmentTypeId")
        {
            (model.DataControl as OABSEngine.DataControls.AppointmentTypeDataControl).UnitId = 9;
        }

        return sr;
    }

Can I call a datacontrol directly?

Yes - purely instantiate the datacontrol - set the relevant parameters and call GetData()



    OABSEngine.DataControls.AppointmentTypeDataControl ADC = new OABSEngine.DataControls.AppointmentTypeDataControl();
    ADC.UnitId = 2;
    ADC.ServiceTypeId = 4;
    List<OABSEngine.Objects.ObjectBase> appointmentTypes = (List<OABSEngine.Objects.ObjectBase>)ADC.GetData();

Keep in mind - all datacontrols return 'object' - so you may need to cast the object to it's required type.

My DataControl didn't return any results. Can I redirect to a different page?

Yes! In this instance - we can use the AfterPopulateDisplayData script event, and specifically the RedirectToStepId parameter.

    public static ScriptResults AfterPopulateDisplayData(JourneyData journeyData, AStepControl stepControl, ref AModel model, ref Guid? redirectedStepId)
    {
        ScriptResults sr = new ScriptResults();

        if (stepControl.FieldName == "AppointmentTypeId")
        {
            if((model as OABSEngine.Models.QFlowObjectModel).DisplayData.Count == 0)
            {
                redirectedStepId = stepControl.Step.Journey.Steps.FirstOrDefault(s => s.CssClass.Contains("NoResults"))?.StepId;
            }
        }

        return sr;
    }

Using the Step CssClass is a useful way of identifying a step. However - we can also use the GetStepControlByFieldName, then navigate up to the Step


    redirectedStepId = stepControl.Step.Journey.GetStepControlByFieldName("NoResults").Step.StepId;

Can I change which page the 'Next' button goes to?

This uses the script method called SelectNextStep. Using this event, we can change the Step we are about to proceed to via several different ways


    public static void SelectNextStep(Journey journey, AStep currentStep, ref int NextPosition, string SubmitValue)
    {
        if (journey.JourneyData.TryGetValue<int>("AppointmentTypeId") == 6)
        {
            NextPosition = journey.Steps.FirstOrDefault(s => s.CssClass.Contains("AppointmentType6")).Position;
        }
        else if(SubmitValue == "GoToUnit")
        {
            NextPosition = journey.Steps.FirstOrDefault(s => s.CssClass.Contains("Units")).Position;
        }
    }

The SubmitValue comes from having a different submit button on the page. These can potentially be added via a CustomListItem



    <button  name="SubmitValue" value="Button1">Button1</button>

    <button  name="SubmitValue" value="Button2">Button2</button>

The 'name' must be 'SubmitValue' value, and the 'value' will be the value picked up in the SelectNextStep script

Can I change the data submitted by a StepControl?

At any point, in most script events, if you have access to the JourneyData object - you can add extra data to it.

However - a common time to add extra information is during the 'Submit' process. A script event called UpdateJourneyData is called, and this can be used to alter the data that is about to be stored int JourneyData.

The data that has been found in a submitted StepControl is in the MappedData. Here, we can add extra MappedData.

For example we have a flow where a customer chooses a ServiceProfile first (queue, or appointment), and then a Unit. We can infer that if they have chosen both, then we can see if there is a single matching Service in the Unit with the ServiceProfile.

If there is - we will also set it as MappedData



    public static ScriptResults UpdateJourneyData(JourneyData journeyData, ref List<KeyValuePair<string, object>> mappedData, ref AStepControl stepControl)
    {
        ScriptResults sr = new ScriptResults();

        if (stepControl.FieldName == "UnitId" && journeyData.ContainsKey("ServiceProfileId"))
        {
            bool isPurged = false;
            int recordCount = 0;

            int ServiceProfileId = journeyData.TryGetValue<int>("ServiceProfileId");
            int unitId = mappedData.TryGetValue<int>("UnitId");

            List<Service> services = Unit.GetServices(unitId, true, recordCount: ref recordCount).Where(s => s.ServiceProfileId == ServiceProfileId).ToList();

            if (services.Count == 1)
            {
                mappedData.TrySetValue("ServiceId", services.First().Id);
            }
        }

        return sr;
    }

Can I stop a datacontrol from populating data?

This is quite simple. In the BeforePopulateDisplayData event - set the ExecuteDataControl parameter as false when you don't want the DataControl to populate data.

You can then proceed to manually poulate the control is required

Can I set two appointments / enqueue two cases at the same time?

This is possible by using a CustomEngineAction. Before OABS attempts a booking or enqueue, it will try to work out from the data provided whether to book, enqueue, reschedule, cancel etc.

This choice can be overridden via the script.



    public static ScriptResults DecideEngineAction(JourneyData journeyData, ref Engine.EngineAction EA)
    {
        ScriptResults sr = new ScriptResults();

        EA = Engine.EngineAction.CustomAction;

        return sr;
    }

From here - instead of the default behaviour, it will execute a CustomEngineAction, which also has a script for it.



    public static ScriptResults ExecuteCustomEngineAction(ref Journey journey)
    {
        ScriptResults sr = new ScriptResults();

        OABSEngine.Objects.Customer Booker = journey.JourneyData.TryGetValue<OABSEngine.Objects.Customer>("Booker");

        int ServiceId1 = journey.JourneyData.TryGetValue<int>("ServiceId1");
        DateTime AppointmentDateTime1 = journey.JourneyData.TryGetValue<DateTime>("AppointmentDateTime1");

        int ServiceId2 = journey.JourneyData.TryGetValue<int>("ServiceId2");
        DateTime AppointmentDateTime2 = journey.JourneyData.TryGetValue<DateTime>("AppointmentDateTime2");

        OABSEngine.Engine.SetAppointment(Booker, new List<OABSEngine.Objects.Customer>(), null, ServiceId1, AppointmentDateTime1, 0, "", "", "", "en", new List<int>(), 1, ref journey);

        OABSEngine.Engine.SetAppointment(Booker, new List<OABSEngine.Objects.Customer>(), null, ServiceId2, AppointmentDateTime2, 0, "", "", "", "en", new List<int>(), 1, ref journey);

        return sr;
    }

How do i set OABS to run in API mode?

For this - you need to set up the OABS web applciation twice, one public facing, and one internal facing (not public facing).

You then set the public facing OABS to UseAPI true, and the BaseURL as the URL of the backend OABS.



    <add key="UseAPI" value="true" />
    <add key="BaseUrl" value="http://[server]/OABSAPI" />

The internal OABS, you set as UseAPI as false, and set a connection string to the database

Can I call DataControls via an API?

If you go to [OABSURL]/help it will give you a list of all public DataControl Apis

These can then be called directly via API call. However - they are secured, so you need to set the header credentials to match what is in the web.config

            
                <add key="DataApiUsername" value="dataapiuser" />
                <add key="DataApiPassword" value="[removed]" />
            

Then we call http://[oabs]/api/ServiceDataControl/GetData?AppointmentTypeId=0&LanguageCode=en&ServiceProfileId=0&ServiceTypeId=0&UnitId=0 with the correct parameters and header and get the results



    [
    {
    "Id": 1,
    "DisplayText": "",
    "CssClass": "",
    "CssStyle": "",
    "JavascriptMethod": null,
    "ContentItems": [
    {
    "ParameterName": "UnitId",
    "Value": "3",
    "ObjectId": 0,
    "ContentObjectType": 0,
    "LanguageCode": "en"
    },
    {
    "ParameterName": "ServiceProfileId",
    "Value": "1",
    "ObjectId": 0,
    "ContentObjectType": 0,
    "LanguageCode": "en"
    },

    ...

Can I View, Reschedule or Cancel a Case/Appointment with OABS?

OABS has a concept called a 'Path' built into it. Each Journey can be split into various pathway, i.e., a booking pathway, a cancellation pathway, a view pathway. By default - these will alter the 'action' that OABS attempts at the end - with a 'Cancel' pathway - if OABS can determine the specificed Case/Process - it will attempt to cancel it. The same goes for rebooking.

When adding a new Step to a Journey, you can specify which path the step should be added to.

Eventually, you can build multiple paths, Book, View, Cancel, Rebook etc, within one Journey.

When a case is booked, you can create a reference token called a 'CaseToken'. In v30 onwards, this is automatically added to the JourneyData after an Action step. This can then be either shown on screen, or added to an email link etc. A url to 'view' the case would look like this: http://[oabs]/appointment/view/[journeyid]?CaseToken=#CaseToken. To start a rebooking journey, it would be http://[oabs]/appointment/rebook/[journeyid]?CaseToken=#CaseToken and cancel would be http://[oabs]/appointment/cancel/[journeyid]?CaseToken=#CaseToken

When navigation to a View, List, Cancel or Rebook path, if a #CaseToken is provided - OABS will preload into the JourneyData several things:

  • The #CaseId containing the value of decrypted CaseToken
  • The #Case object. This is obtained from the 'CaseListDataControl' using the CaseId to get a single result, and contains all the CaseData (ServiceName, UnitName, CustomerFirstName etc).
  • The #OriginalProcessId used for Cancelling/Rescheduling by OABS

We can now show Case information in two different ways, either populating a Model such as a ListItemModel with a CaseListDataControl

Or via referencing the Case object in JouneyData using the new #{A.B.C} notations

For actions such as rescheduling - we would be interested in populated the selection criteria with the values of the original booking. OABS is designed to handle this in as straightforward manner as possible. When adding StepControls to the 'rebook' journey, we can specify that the Model should 'Preload'

We can indicate the source of the 'Preload', either from a 'Key' in the Query string, or from the inbuilt 'Case' object. This will look for a property in the Case object matching the FieldName (or for more complex objects such as ServiceAppointmentDateTime, it will look for ServiceId, ReferenceDate and AppointmentId automatically)

We can of course still use script such as PreloadJourney to prepopulate model values

How do I ensure performance / Can I use caching within OABS script?

In OABS, we can use script to alter the business logic behind each step. As part of this - developers are free to write code to query the QFlow database, or other datasources. In having this freedom - a developer needs to make sure to be very careful not to create unecessary load on the system.

A common mistake is to do something like querying the database in a 'AfterPopulateDisplayData' method - which may be a looped, heavily used method, and can hit a database very hard.

Therefore, it is important to utilise caching techniques where possible.

Firstly - OABS contains several helpers to cache data:

            
                string MemoryCacheKey = String.Format("A_unique_key_for_the_object");

                MyCustomObject MCO = OABSEngine.Helpers.Caching.Get<MyCustomObject>(MemoryCacheKey);

                if (MCO == null)
                {
                    string connectionString = ConfigurationUtil.ConnectionStrings("QFlowDB").ConnectionString;
                    using (SqlConnection sqlConnection = new SqlConnection(connectionString))
                    {
                        using (SqlCommand sqlCommand = new SqlCommand())
                        {
                            //Code to get a MCO data from the database
                        }
                    }

                    OABSEngine.Helpers.Caching.Set(MemoryCacheKey, MCO, "", TimeSpan.FromMinutes(1));
                }
            
        

Alternately - we can cache a method call, such as caching the results from a QFlow library call, using the following helper:

                
var AppointmentTypes = OABSEngine.Helpers.Caching.CachedMethodCall(() => QFlow.Library.AppointmentType.GetAll(1, true, "", false), new TimeSpan(0,1,0));
                
            

How can I execute manual validation on a page?

In v29 or earlier, it is not possible without clientside javascript

In v30 onwards, we use a script event called ValidateStepControlValues

For example - if we set up a Step with two step controls - two string models for username and password:

We can now use the script event to do validation, as follows:

                
public static ScriptResults ValidateStepControlValues(ref AStep sourceStep, ref Journey journey, ref bool ModelStateValid)
{
    ScriptResults sr = new ScriptResults();

    if (sourceStep.StepControls.Any(sc=>sc.FieldName=="Username"))
    {
        var UN = sourceStep.StepControls.FirstOrDefault(sc => sc.FieldName == "Username").Model.Value.ToString();
        var PW = sourceStep.StepControls.FirstOrDefault(sc => sc.FieldName == "Password").Model.Value.ToString();

        if (UN != PW)
        {
            sourceStep.ModelValidationErrors.ModelValidationItems.Add(new OABSEngine.Objects.ModelValidationItem()
            {
                StepControlId = sourceStep.StepControls.FirstOrDefault(sc => sc.FieldName == "Username").StepControlId.ToString(),
                ValidationMessageTitle = "Here's a custom title",
                ValidationMessage = "here's a custom message"
            });

            sourceStep.ModelValidationErrors.Header = "Here's a custom error title";
            sourceStep.ModelValidationErrors.SubHeader = "Here's a custom error subtitle";

            ModelStateValid = false;
        }
    }

    return sr;
}
                
        

This will result in a bespoke validation message being displayed

How can I get items from Content?

OABS will automatically build up a library of Content Items on startup, and maintain these in cache during use.

We can get content by callinging ContentItem.Get, passing through the type and ID or LanguageCode

                
                    var Global = OABSEngine.Objects.ContentItem.Get(OABSEngine.Objects.ContentObjectType.Global, 0);
                    var AT = OABSEngine.Objects.ContentItem.Get(OABSEngine.Objects.ContentObjectType.AppointmentType, 1);
                    var S = OABSEngine.Objects.ContentItem.Get(OABSEngine.Objects.ContentObjectType.Service, 0, "en");
                
        

We can now filter on Language if need be