Create a Reusable UI Model Helper Class
When you develop data forms, you often need to reuse some code across add forms and edit forms. One big challenge is that VB.Net (and C# for that matter) doesn't support multiple inheritance. Hypothetically, if it did, we could create an abstract class that both UI model classes inherit from. Unfortunately we don't have that, so get that thought out of your head. However, there is a convenient way to do this with a helper class. This article shows an example on how to create a helper class for your UI model forms.
For this article, I have an example of a constituent pet form that let's you add a pet for a constituent. The requirement we need here is to enable the vaccination date when users select the checkbox for distemperment vaccination. You can downlowd the source code for the sample here.
Here are the specs for the table, add and edit form for reference:
<TableSpec
xmlns="bb_appfx_table"
xmlns:common="bb_appfx_commontypes"
ID="3c095dad-6726-474a-a585-91c6900cbe88"
Name="Constituent Pet"
Description="Store constituent pet information."
Author="Blackbaud Professional Services"
Tablename="USR_CONSTITUENTPET"
>
<Fields>
<ForeignKeyField
Name="CONSTITUENTID"
ForeignTable="CONSTITUENT"
Required="true"
OnDelete="CascadeDelete"/>
<EnumField
Name="PETTYPECODE"
DefaultValue="0">
<EnumValues>
<EnumValue ID="0" Translation="Dog"/>
<EnumValue ID="1" Translation="Cat"/>
</EnumValues>
</EnumField>
<TextField
Name="PETNAME"
Length="100"
Required="true"/>
<BooleanField
Name="DISTEMPERVACCINATION"/>
<DateField
Name="DISTEMPERVACCINATIONDATE"/>
</Fields>
<Indexes>
<Index>
<IndexFields>
<IndexField Name="CONSTITUENTID"/>
</IndexFields>
</Index>
</Indexes>
</TableSpec>
<AddDataFormTemplateSpec
xmlns:c="bb_appfx_commontypes"
ID="e3632018-f68f-427d-a748-fd5df2ae4947"
Name="Constituent Pet Add Form"
Description="Used for adding a new constituent pets."
Author="Blackbaud Professional Services"
RecordType="Constituent Pet"
DataFormInstanceID="fddf7267-a0b1-45f7-8a45-ba518577236c"
c:SecurityUIFolder="Constituent Pet"
xmlns="bb_appfx_adddataformtemplate"
FormHeader="Add pet"
>
<SPDataForm>
<SaveImplementation SPName="USR_USP_DATAFORMTEMPLATE_ADD_CONSTITUENT_PET">
<c:CreateProcedureSQL>
<! [CDATA[
create procedure dbo.USR_USP_DATAFORMTEMPLATE_ADD_CONSTITUENT_PET
(
@ID uniqueidentifier = null output,
@CHANGEAGENTID uniqueidentifier = null,
@CONSTITUENTID uniqueidentifier,
@PETTYPECODE tinyint = 0,
@PETNAME nvarchar(100),
@DISTEMPERVACCINATION bit = 0,
@DISTEMPERVACCINATIONDATE datetime = null
)
as
set nocount on;
if @ID is null
set @ID = newid()
if @CHANGEAGENTID is null
exec dbo.USP_CHANGEAGENT_GETORCREATECHANGEAGENT @CHANGEAGENTID output
declare @CURRENTDATE datetime
set @CURRENTDATE = getdate()
begin try
-- handle inserting the data
insert into dbo.USR_CONSTITUENTPET(
ID,
CONSTITUENTID,
PETTYPECODE,
PETNAME,
DISTEMPERVACCINATION,
DISTEMPERVACCINATIONDATE,
ADDEDBYID,
CHANGEDBYID,
DATEADDED,
DATECHANGED)
values(
@ID,
@CONSTITUENTID,
@PETTYPECODE,
@PETNAME,
@DISTEMPERVACCINATION,
case
when @DISTEMPERVACCINATION = 1 then @DISTEMPERVACCINATIONDATE
else null
end,
@CHANGEAGENTID,
@CHANGEAGENTID,
@CURRENTDATE,
@CURRENTDATE)
end try
begin catch
exec dbo.USP_RAISE_ERROR
return 1
end catch
return 0
] ]>
</c:CreateProcedureSQL>
<c:ExpectedDBExceptions>
<c:Constraints>
<c:Constraint Name="CK_USR_CONSTITUENTPET_PETNAME" Field="PETNAME" Type="Required" />
</c:Constraints>
</c:ExpectedDBExceptions>
</SaveImplementation>
</SPDataForm>
<Context ContextRecordType="Constituent" RecordIDParameter="CONSTITUENTID" />
<c:FormMetaData FixedDialog="true">
<c:FormFields>
<c:FormField FieldID="PETTYPECODE" DataType="TinyInt" Caption="Type" DefaultValueText="0" Required="true">
<c:ValueList>
<c:Items>
<c:Item>
<c:Value>0</c:Value>
<c:Label>Dog</c:Label>
</c:Item>
<c:Item>
<c:Value>1</c:Value>
<c:Label>Cat</c:Label> </c:Item>
</c:Items>
</c:ValueList>
</c:FormField>
<c:FormField FieldID="PETNAME" Required="true" MaxLength="100" Caption="Name" />
<c:FormField FieldID="DISTEMPERVACCINATION" DataType="Boolean" Caption="Distemperment vaccination" />
<c:FormField FieldID="DISTEMPERVACCINATIONDATE" DataType="Date" Caption="Vaccination date" />
</c:FormFields>
<c:FormUIComponent />
<c:WebUIComponent>
<c:UIModel AssemblyName="ConstituentPet.UIModel.dll" ClassName="ConstituentPet.UIModel.ConstituentPetAddFormUIModel" />
<c:WebUI>
<c:DefaultWebUI />
</c:WebUI>
</c:WebUIComponent>
</c:FormMetaData>
</AddDataFormTemplateSpec>
<EditDataFormTemplateSpec
xmlns:c="bb_appfx_commontypes"
ID="30fbe1b7-b39f-42c7-9475-063cc5dabb36"
Name="Constituent Pet Edit Form"
Description="Used for editing the a constituent pet."
Author="Blackbaud Professional Services"
RecordType="Constituent Pet"
DataFormInstanceID="d73c375e-6432-40db-90f5-9a2eee901c5a"
c:SecurityUIFolder="Constituent Pet"
OwnerIDMapperID="00000000-0000-0000-0000-000000000000"
xmlns="bb_appfx_editdataformtemplate"
FormHeader="Edit pet"
>
<SPDataForm>
<LoadImplementation SPName="USR_USP_DATAFORMTEMPLATE_EDITLOAD_CONSTITUENT_PET">
<c:CreateProcedureSQL>
<![ CDATA[
create procedure dbo.USR_USP_DATAFORMTEMPLATE_EDITLOAD_CONSTITUENT_PET
(
@ID uniqueidentifier,
@DATALOADED bit = 0 output,
@TSLONG bigint = 0 output,
@PETTYPECODE tinyint = null output,
@PETNAME nvarchar(100) = null output,
@DISTEMPERVACCINATION bit = null output,
@DISTEMPERVACCINATIONDATE datetime = null output
)
as
set nocount on;
-- be sure to set these, in case the select returns no rows
set @DATALOADED = 0
set @TSLONG = 0
-- populate the output parameters, which correspond to fields on the form. Note that
-- we set @DATALOADED = 1 to indicate that the load was successful. Otherwise, the system
-- will display a "no data loaded" message. Also note that we fetch the TSLONG so that concurrency
-- can be considered.
select
@DATALOADED = 1,
@TSLONG = TSLONG,
@PETTYPECODE = PETTYPECODE,
@PETNAME = PETNAME,
@DISTEMPERVACCINATION = DISTEMPERVACCINATION,
@DISTEMPERVACCINATIONDATE = DISTEMPERVACCINATIONDATE
from dbo.USR_CONSTITUENTPET
where ID = @ID
return 0;
] ]>
</c:CreateProcedureSQL>
</LoadImplementation>
<SaveImplementation SPName="USR_USP_DATAFORMTEMPLATE_EDIT_CONSTITUENT_PET">
<c:CreateProcedureSQL>
<![ CDATA[
create procedure dbo.USR_USP_DATAFORMTEMPLATE_EDIT_CONSTITUENT_PET
(
@ID uniqueidentifier,
@CHANGEAGENTID uniqueidentifier = null,
@PETTYPECODE tinyint,
@PETNAME nvarchar(100),
@DISTEMPERVACCINATION bit,
@DISTEMPERVACCINATIONDATE datetime
)
as
set nocount on;
if @CHANGEAGENTID is null
exec dbo.USP_CHANGEAGENT_GETORCREATECHANGEAGENT @CHANGEAGENTID output
begin try
-- handle updating the data
update dbo.USR_CONSTITUENTPET set
PETTYPECODE = @PETTYPECODE,
PETNAME = @PETNAME,
DISTEMPERVACCINATION = @DISTEMPERVACCINATION,
DISTEMPERVACCINATIONDATE = case
when @DISTEMPERVACCINATION = 1 then @DISTEMPERVACCINATIONDATE
else null
end,
CHANGEDBYID = @CHANGEAGENTID,
DATECHANGED = getdate()
where ID = @ID
end try
begin catch
exec dbo.USP_RAISE_ERROR
return 1
end catch
return 0;
] ]>
</c:CreateProcedureSQL>
<c:ExpectedDBExceptions>
<c:Constraints>
<c:Constraint Name="CK_USR_CONSTITUENTPET_PETNAME" Field="PETNAME" Type="Required" />
</c:Constraints>
</c:ExpectedDBExceptions>
</SaveImplementation>
</SPDataForm>
<c:FormMetaData FixedDialog="true">
<c:FormFields>
<c:FormField FieldID="PETTYPECODE" DataType="TinyInt" Caption="Type" DefaultValueText="0" Required="true">
<c:ValueList>
<c:Items>
<c:Item>
<c:Value>0</c:Value>
<c:Label>Dog</c:Label>
</c:Item>
<c:Item>
<c:Value>1</c:Value>
<c:Label>Cat</c:Label>
</c:Item>
</c:Items>
</c:ValueList>
</c:FormField>
<c:FormField FieldID="PETNAME" Required="true" MaxLength="100" Caption="Name" />
<c:FormField FieldID="DISTEMPERVACCINATION" DataType="Boolean" Caption="Distemperment vaccination" />
<c:FormField FieldID="DISTEMPERVACCINATIONDATE" DataType="Date" Caption="Vaccination date" />
</c:FormFields>
<c:FormUIComponent />
<c:WebUIComponent>
<c:UIModel AssemblyName="ConstituentPet.UIModel.dll" ClassName="ConstituentPet.UIModel.ConstituentPetEditFormUIModel" />
<c:WebUI>
<c:DefaultWebUI />
</c:WebUI>
</c:WebUIComponent>
</c:FormMetaData>
</EditDataFormTemplateSpec>
Now we want to implement this logic across both forms, but we want to do it just once. Obviously in this example, the cost of implementing this twice is low and the likelihood of new functionality is low. However, thinking in terms of a larger form with complicated logic, it is very helpful.
Here is our helper class.
Public NotInheritable Class ConstituentPetHelper
Private _model As UIModeling.Core.RootUIModel
Public Sub New(ByVal model As UIModeling.Core.RootUIModel)
_model = model
AddHandler model.Fields("DISTEMPERVACCINATION").ValueChanged, AddressOf DistemperVaccination_ValueChanged
ToggleDistemperVacciationDate()
End Sub
Private Sub DistemperVaccination_ValueChanged(ByVal sender As Object, ByVal e As Blackbaud.AppFx.UIModeling.Core.ValueChangedEventArgs)
ToggleDistemperVacciationDate()
End Sub
Private Sub ToggleDistemperVacciationDate()
If CBool(_model.Fields("DISTEMPERVACCINATION").ValueObject) Then
_model.Fields("DISTEMPERVACCINATIONDATE").Enabled = True
Else
_model.Fields("DISTEMPERVACCINATIONDATE").Enabled = False
_model.Fields("DISTEMPERVACCINATIONDATE").ValueObject = Nothing
End If
End Sub
End Class
Now to use this in our model code, we just need to create a variable with them.
Public Class ConstituentPetAddFormUIModel
<System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1804:RemoveUnusedLocals", MessageId:="petHelper")> _
Private Sub ConstituentPetAddFormUIModel_Loaded(ByVal sender As Object, ByVal e As Blackbaud.AppFx.UIModeling.Core.LoadedEventArgs) Handles Me.Loaded
Dim petHelper As New ConstituentPetHelper(Me)
End Sub
End Class
Public Class ConstituentPetEditFormUIModel
<System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1804:RemoveUnusedLocals", MessageId:="petHelper")> _
Private Sub ConstituentPetEditFormUIModel_Loaded(ByVal sender As Object, ByVal e As Blackbaud.AppFx.UIModeling.Core.LoadedEventArgs) Handles Me.Loaded
Dim petHelper As New ConstituentPetHelper(Me)
End Sub
End Class
Note: I suppressed the code warning as it complains about an unused local variable. In this example, you couldn't avoid that, but there could be cases in which you have a global variable that stores the helper for later access.
Here are some screen shots of the final product.
You can download the source code for the sample here.