Save Implementations
Save implementations inform the platform about how to insert or update form field values in database tables when a form is saved. Save implementations can be defined through a CREATE PROCEDURE statement declared inline with the spec XML or common language runtime code in classes referenced by the spec. Save implementations are required for Add Data Forms and Edit Data Forms.
Stored Procedure Save Implementations
The save implementation for a stored procedure data form spec is defined within the SPDataForm element in a SaveImplementation child element. SaveImplementation must have an SPName attribute that defines the stored procedure name. Although the SPSchema attribute appears as an IntelliSense option, SPSchema is not applicable to save implementations for data forms.
The names for custom stored procedures for load implementations and save implementations should begin with USR_USP_DATAFORMTEMPLATE_. The USR_USP_DATAFORMTEMPLATE_ prefix indicates by convention that the stored procedure is a custom stored procedure for a Blackbaud Infinity data form instance. For Add Data Forms, USR_USP_DATAFORMTEMPLATE_ should be followed by ADD_. For Edit Data Forms, USR_USP_DATAFORMTEMPLATE_ should be followed by EDIT_. All of that should be followed by a string to indicate the record type and possibly the data form instance. For example:
USR_USP_DATAFORMTEMPLATE_ADD_CUSTOMRECORD
USR_USP_DATAFORMTEMPLATE_EDIT_CUSTOMRECORD
Within the SaveImplementation element, a CreateProcedureSQL element with the bb_appfx_commontypes namespace should wrap a CDATA that wraps the Transact-SQL for the CREATE PROCEDURE statement. For specs created with the SDK wizard, the namespace prefix established in the root element for bb_appfx_commontypes is common. The convention for Transact-SQL used by Blackbaud Infinity is to use lower-case for keywords. The name for the procedure is repeated by the CREATE PROCEDURE statement and preceded by the schema used by the transactional database for Blackbaud Infinity applications. For more information about create procedure statements, see the MSDN topic CREATE PROCEDURE (Transact-SQL).
<SaveImplementation SPName="USR_USP_DATAFORMTEMPLATE_ADD_EXAMPLESP">
<common:CreateProcedureSQL>
<![CDATA[
create procedure dbo.USR_USP_DATAFORMTEMPLATE_ADD_EXAMPLESP
..
] ]>
</common:CreateProcedureSQL>
</SaveImplementation>
</SPDataForm>
Parameters can be passed to stored procedures for save implementations. A spec created by the wizard includes six parameters for the purposes of illustration.
create procedure dbo.USR_USP_DATAFORMTEMPLATE_ADD_EXAMPLESP
(
@ID uniqueidentifier = null output,
@CONTEXTID uniqueidentifier,
@CHANGEAGENTID uniqueidentifier = null,
@FIELD1 nvarchar(10) = '',
@FIELD2 nvarchar(20) = '',
@FIELD3 nvarchar(max) = ''
)
The parameter declarations indicate a type such as uniqueidentifier or nvarchar(10) in the example. A default may be assigned to a parameter. For @ID in the example, a default null is assigned and the parameter is declared to be an output parameter to be returned by the stored procedure.
The @ID parameter represents the record ID for the record to be loaded. When a table is created in an Infinity application, the platform creates this column. The @CHANGEAGENTID parameter also corresponds to a column created by the platform. The ADDEDBYID and CHANGEDBYID columns store uniqueidentifiers that represent the ID for application users or a process changing something in the database. The @ID and @CHANGEAGENTID parameters and ID, ADDEDBYID, and CHANGEDBYID are type uniqueidentifier. Values pass between the @ID and @CHANGEDBYID parameters and the ID, ADDEDBYID, and CHANGEDBYID in an INSERT or UPDATE statement within the stored procedure.
Some forms require context, an ID for a parent record. For example a record for a car model may require a context record ID for car make. If the form expects context, one of the stored procedure parameters may be referenced elsewhere in the spec in a Context element. A parameter used for context does not have to be called @CONTEXT. The name is referenced in the RecordIDParameter attribute in the Context element.
This line indicates to not return the count of records:
set nocount on;
For more information about set nocount, see the MSDN topic SET NOCOUNT (Transact-SQL). This is suggested as a possible best practice to improve performance in the MSDN topic CREATE PROCEDURE (Transact-SQL).
For Edit Data Form save implementations, the first conditional in the following snippet is not necessary because a record already exists. But for Add Data Form save implementations, a new uniqueidentifier is required for the record to be added. A uniqueidentifier is returned by newid(), a system function in SQL Server databases. For more information about newid(), see the MSDN topic NEWID (Transact-SQL).
The rest of the snippet is applicable to Add Data Form and Edit Data Form save implementations. The stored procedure dbo.USP_CHANGEAGENT_GETORCREATECHANGEAGENT is part of the Blackbaud Infinity platform. The dbo.CHANGEAGENT table stores the IDs and other information for change agents. The dbo.USP_CHANGEAGENT_GETORCREATECHANGEAGENT stored procedure in this case returns ID to @CHANGEAGENTID since it is passed to the @ID input/output parameter for the stored procedure.The function getdate() is a built-in date function. For more information about getdate(), see the MSDN topic GETDATE (Transact-SQL).
if @ID is null
set @ID = newid()
if @CHANGEAGENTID is null
exec dbo.USP_CHANGEAGENT_GETORCREATECHANGEAGENT @CHANGEAGENTID output
declare @CURRENTDATE datetime
set @CURRENTDATE = getdate()
Edit Data Form save implementations include some variation of an update statement. For more information about update statements, see the MSDN topic UPDATE (Transact-SQL). Fields in a database table row are assigned values such as those passed to the stored procedure as parameters as with FIELD1, FIELD2, and FIELD3 in the example or with values retrieved from a procedure or function call such as with CHANGEDBYID and DATECHANGED. Other logic can be used within the stored procedure to determine values to update.
begin try
-- handle updating the data
update dbo.TABLE1 set
FIELD1 = @FIELD1,
FIELD2 = @FIELD2,
FIELD2 = @FIELD3,
CHANGEDBYID = @CHANGEAGENTID,
DATECHANGED = @CURRENTDATE
where ID = @ID
end try
begin catch
exec dbo.USP_RAISE_ERROR
return 1
end catch
Add Data Form save implementations include some variation of an insert statement. For more information about insert statements, see the MSDN topic INSERT (Transact-SQL). Fields in a new database table row are assigned values such as those passed to the stored procedure as parameters as with FIELD1, FIELD2, and FIELD3 in the example or with values retrieved from a procedure or function call such as with CHANGEDBYID and DATECHANGED. Other logic can be used within the stored procedure to determine values to update.
begin try
-- handle inserting the data
insert into dbo.TABLE1
(ID, FIELD1, FIELD2, FIELD3, ADDEDBYID, CHANGEDBYID, DATEADDED, DATECHANGED)
values
(@ID, @FIELD1, @FIELD2, @FIELD3, @CHANGEAGENTID, @CHANGEAGENTID, @CURRENTDATE, @CURRENTDATE)
end try
begin catch
exec dbo.USP_RAISE_ERROR
return 1
end catch
A Transact-SQL RETURN statement is an exit statement that returns a status. The following stops execution of the query and returns a status of 0. If an error is encountered in the preceding try-catch statement, the returned value is 1, indicating an error. For more information about return statements, see the MSDN topic RETURN (Transact-SQL).
return 0;
Common Language Runtime Save Implementations
CLR (Common Language Runtime) data form specs enable business logic beyond reading and writing to the application's transactional database. Two scenarios where this may be helpful are when reading data from an external source such as a web service and to create dynamic Transact-SQL with which to read or write to the application's transactional database.
When a CLR data form spec is created with the SDK wizard, the initial class file for the implementations contains overridden functions for the load and save logic. These functions are contained in a base class designed for to the spec type, in this case Blackbaud.AppFx.Server.AppCatalog.AppEditDataForm.
Imports Blackbaud.AppFx.Server Public NotInheritable Class ExampleClrEditDataForm Inherits AppCatalog.AppEditDataForm Public Overrides Function Load() As Blackbaud.AppFx.Server.AppCatalog.AppEditDataFormLoadResult End Function Public Overrides Function Save() As Blackbaud.AppFx.Server.AppCatalog.AppEditDataFormSaveResult End Function End Class
There are two properties inherited from the base class to help access information about the request and context: RequestArgs, and ProcessContext.
RequestArgs contains information about the original request that initiated the invocation of the CLR implementation. This includes ClientAppInfo and UsesDatabaseConnection. ProcessContext contains other useful information.
RequestContext is a member of Blackbaud.AppFx.Server.dll and encapsulates information about a request and provides access to server utility classes and properties.
To read and write to the database, Transact-SQL code can be created within the Load() or Save() functions and executed with methods in a library such as SqlClient. Accesses to other data sources, whether outside of the application or the application services, can be made using the API for those services.
To replicate the behavior of the illustrative stored procedure created by the SDK wizards Transact-SQL can be executed with SqlClient.
Closing Connections
Sometimes your code requires an unmanaged resource, such as a SQL connection. If the SqlConnection goes out of scope, it won't be closed. Therefore, you must explicitly close the connection by calling Close or Dispose. You may also consider a Using block which guarantees the disposal of one or more such resources when your code is finished with them. For more information see http://msdn.microsoft.com/en-us/library/system.data.sqlclient.sqlconnection.close.aspx and http://msdn.microsoft.com/en-us/library/htd05whh.aspx.
For example, the following code would require the explicit closing of the connection:
Dim conn As SqlConnection = Me.RequestContext.OpenAppDBConnection
‘code to perform work with the sql connection
conn.Close
Whereas utilizing a Using block guarantees the disposal of the SQLConnection:
using conn As SqlConnection = Me.RequestContext.OpenAppDBConnection
‘code to perform work with the sql connection
end using
Imports Blackbaud.AppFx.Server
Public NotInheritable Class Ex3EditDataForm
Inherits AppCatalog.AppEditDataForm
Public FIELD1 As String
Public FIELD2 As String
Public Overrides Function Load() As Blackbaud.AppFx.Server.AppCatalog.AppEditDataFormLoadResult
Dim dataFormLoadResult As New AppCatalog.AppEditDataFormLoadResult(False)
Using con As SqlClient.SqlConnection = Me.RequestContext.OpenAppDBConnection()
Using command As SqlClient.SqlCommand = con.CreateCommand()
command.CommandText = _
"select " & " FIELD1, FIELD2 " & _
"from dbo.USR_EX " & _
"where dbo.USR_EX.ID = @ID;"
command.Parameters.AddWithValue("@ID", New Guid(Me.ProcessContext.RecordID))
Using reader As SqlClient.SqlDataReader = command.ExecuteReader()
reader.Read()
FIELD1 = reader.GetString(reader.GetOrdinal("FIELD1"))
FIELD2 = reader.GetString(reader.GetOrdinal("FIELD2"))
dataFormLoadResult.DataLoaded = True
reader.Close()
End Using
command.Dispose()
End Using
End Using
Return dataFormLoadResult
End Function
Public Overrides Function Save() As Blackbaud.AppFx.Server.AppCatalog.AppEditDataFormSaveResult
Dim dataFormSaveResult As New AppCatalog.AppEditDataFormSaveResult()
Try
Using con As SqlClient.SqlConnection = Me.RequestContext.OpenAppDBConnection()
Using command As SqlClient.SqlCommand = con.CreateCommand()
command.CommandText = _
"update " & " dbo.USR_EX set " & _
"FIELD1 = @FIELD1, " & _
"FIELD2 = @FIELD2, " & _
"DATECHANGED = getdate(), " & _
"CHANGEDBYID = @CHANGEAGENTID " & _
"where ID = @ID;"
command.Parameters.AddWithValue("@ID", New Guid(Me.ProcessContext.RecordID))
command.Parameters.AddWithValue("@FIELD1", FIELD1)
command.Parameters.AddWithValue("@FIELD2", FIELD2)
Dim changeAgent As Nullable(Of Guid) = Me.RequestContext.GetChangeAgentID()
command.Parameters.AddWithValue("@CHANGEAGENTID", changeAgent)
command.ExecuteNonQuery()
command.Dispose()
End Using
End Using
Catch ex As Exception
Throw
End Try
Return dataFormSaveResult
End Function
End Class