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.
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.
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}
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
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/
<compilation debug="false" targetFramework="4.7.2" />
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:
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
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();
}
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:
&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.
&CustomerId={X}
to search QFlow for that customer, and set it in the #Booker
StepControl modelThere 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;
}
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 thehttpErrors
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
#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:
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
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
After all of these, OABS will attempt to resolve any remaining # parameters using the built in QFlow DynamicText.Resolve methods.
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.
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:
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()
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;
}
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.
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;
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
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;
}
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
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;
}
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
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"
},
...
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:
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
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));
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
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