When adding an export script to a document class the RunUI method gets fired and shows the setup form. When removing the script this happens too. I would like to prevent it because there is no need for it.
In my ActionEvent method I could create a switch for the KfxActionValue.
I don't want to show the UI when removing the script but I want to show it when adding the script or when I want to edit it.
The documentation is not very helpful as you can see here
I took the Sharepoint example and the KCEC Text example and created this
public KfxReturnValue ActionEvent(KfxActionValue actionID, string data1, string data2)
{
try
{
bool showUI = false;
switch (actionID)
{
case KfxActionValue.KFX_REL_INDEXFIELD_INSERT:
case KfxActionValue.KFX_REL_INDEXFIELD_DELETE:
case KfxActionValue.KFX_REL_BATCHFIELD_INSERT:
case KfxActionValue.KFX_REL_BATCHFIELD_DELETE:
showUI = true;
break;
//case KfxActionValue.KFX_REL_UNDEFINED_ACTION:
//case KfxActionValue.KFX_REL_DOCCLASS_RENAME:
//case KfxActionValue.KFX_REL_BATCHCLASS_RENAME:
//case KfxActionValue.KFX_REL_INDEXFIELD_RENAME:
//case KfxActionValue.KFX_REL_BATCHFIELD_RENAME:
//case KfxActionValue.KFX_REL_RELEASESETUP_DELETE:
//case KfxActionValue.KFX_REL_IMPORT:
//case KfxActionValue.KFX_REL_UPGRADE:
//case KfxActionValue.KFX_REL_PUBLISH_CHECK:
//case KfxActionValue.KFX_REL_START:
//case KfxActionValue.KFX_REL_END:
//case KfxActionValue.KFX_REL_FOLDERCLASS_INSERT:
//case KfxActionValue.KFX_REL_FOLDERCLASS_RENAME:
//case KfxActionValue.KFX_REL_FOLDERCLASS_DELETE:
//case KfxActionValue.KFX_REL_TABLE_DELETE:
//case KfxActionValue.KFX_REL_TABLE_INSERT:
//case KfxActionValue.KFX_REL_TABLE_RENAME:
//default:
// break;
}
if (showUI)
{
return RunUI();
}
return KfxReturnValue.KFX_REL_SUCCESS;
}
catch (Exception e)
{
setupData.LogError(e.ToString());
throw e;
}
}
but I'm not sure if this is correct. It works. But where can I get more information about it?
Your approach is correct. There isn't much more information about those action events in the official documentation, yet the basic concept is explained in the Developer's Guide (you will need a valid Kofax account to download the documentation).
Here's my cheat sheet:
KFX_REL_UNDEFINED_ACTION - unknown
KFX_REL_DOCCLASS_RENAME - renamed the associated document class(es)
KFX_REL_BATCHCLASS_RENAME - renamed the batch class
KFX_REL_INDEXFIELD_DELETE - removed an existing index
KFX_REL_INDEXFIELD_INSERT - added a new index field
KFX_REL_INDEXFIELD_RENAME - renamed an existing field
KFX_REL_BATCHFIELD_DELETE - removed an existing batch field
KFX_REL_BATCHFIELD_INSERT - added a new batch field
KFX_REL_BATCHFIELD_RENAME - renamed an existing field
KFX_REL_RELEASESETUP_DELETE - removed the Export Connector from the document class
KFX_REL_IMPORT - called when the batch class is imported (not tested!)
KFX_REL_UPGRADE - called when the user hits the "Upgrade" button (must be supported by the Export Connector)
KFX_REL_PUBLISH_CHECK - called when the batch class is validated or published
KFX_REL_START - called whenever the release (setup) script is called, for example when adding an index field
KFX_REL_END - called when the setup script is closed
KFX_REL_FOLDERCLASS_INSERT - added a new folder class
KFX_REL_FOLDERCLASS_RENAME - renamed an existing folder class
KFX_REL_FOLDERCLASS_DELETE - removed an existing folder class
KFX_REL_TABLE_DELETE - deleted an existing table
KFX_REL_TABLE_INSERT - inserted a new table
KFX_REL_TABLE_RENAME - renamed an existing table
Note that some events will be fired in succession. For example, renaming the batch will fire KFX_REL_START, then KFX_REL_BATCHCLASS_RENAME, and finally KFX_REL_END.
Related
I have a custom module getting executed right after the PDFGenerator finished. I followed this guide on how to create a custom module
https://stackoverflow.com/a/55799101/9945420
When processing a batch document I want to manipulate the generated PDF file and add a footer to that file. The content of that footer needs to get configured in the Administration module.
So within my project called "StampOnScanProcess" I added a Folder called "Setup" with two files. A Form called "FrmSetup"
public partial class FrmSetup : Form
{
private IBatchClass batchClass;
public FrmSetup()
{
InitializeComponent();
}
public DialogResult ShowDialog(IBatchClass batchClass)
{
this.batchClass = batchClass;
// Load previous Settings ...
return this.ShowDialog();
}
private void btnCancel_Click(object sender, EventArgs e)
{
this.Close();
}
private void btnSave_Click(object sender, EventArgs e)
{
// Save ...
this.Close();
}
}
and a UserControl called "UserCtrlSetup"
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface ISetupForm
{
[DispId(1)]
AdminApplication Application { set; }
[DispId(2)]
void ActionEvent(int EventNumber, object Argument, out int Cancel);
}
[ClassInterface(ClassInterfaceType.None)]
[ProgId(CUSTOM_MODULE_NAME_SETUP)]
public partial class UserCtrlSetup : UserControl, ISetupForm
{
private const string CUSTOM_MODULE_NAME_SETUP = "StampOnScanProcess.Setup";
private AdminApplication adminApplication;
public AdminApplication Application
{
set
{
value.AddMenu(CUSTOM_MODULE_NAME_SETUP, CUSTOM_MODULE_NAME_SETUP, "BatchClass");
adminApplication = value;
}
}
public void ActionEvent(int EventNumber, object Argument, out int Cancel)
{
Cancel = 0;
if ((KfxOcxEvent)EventNumber == KfxOcxEvent.KfxOcxEventMenuClicked && (string)Argument == CUSTOM_MODULE_NAME_SETUP)
{
FrmSetup form = new FrmSetup();
form.ShowDialog(adminApplication.ActiveBatchClass);
}
}
}
I modified my registration file and added the setup form to it
[Modules]
StampOnScanProcess
[StampOnScanProcess]
RuntimeProgram=StampOnScanProcess.exe
ModuleID=StampOnScanProcess.exe
Description=...
Version=10.2
SupportsNonImageFiles=True
SupportsTableFields=True
SetupProgram=StampOnScanProcess.Setup
[Setup Programs]
StampOnScanProcess.Setup
[StampOnScanProcess.Setup]
Visible=0
OCXFile=StampOnScanProcess.exe
ProgID=StampOnScanProcess.Setup
When launching the Administration module I head over to the Batch Class Properties => Queues and want to call this setup form by clicking the Properties button in the middle.
Unfortunately the properties button is disabled so I can't open the setup form. This form gets added to the context menu of the batch class
How can I bind this form to the properties button instead? And what is the best way to store configured data and access it when the runtime application gets executed?
I need to think about how to store data because some users have user profiles
and the runtime application currently logs in with no credentials.
public void LoginToRuntimeSession()
{
login = new Login();
login.EnableSecurityBoost = true;
login.Login();
login.ApplicationName = CUSTOM_MODULE_ID;
login.Version = "1.0";
login.ValidateUser($"{CUSTOM_MODULE_ID}.exe", false, "", "");
session = login.RuntimeSession;
}
So it might happen that I have to store the credentials on setup too.
How can I bind this form to the properties button instead?
All interactions with menu entries are handled by ISetupForm.ActionEvent. New entries are added with the AddMenu method of the AdminApplication object. Kofax differentiates between multiple entries by name - imagine that you could have multiple menu entries at the same time, one on batch class level, another one on document class level, and another one in the ribbon - just to name a few examples. Kofax uses the same approach in any component that integrates into Administration (e.g. Custom Modules or Workflow Agents).
This is an example from one of our components. Note that three entries are added on BatchClass level and two more on DocumentClass level.
value.AddMenu("BatchClass.GeneralConfig", "Field Panel - General Configuration", "BatchClass");
value.AddMenu("BatchClass.FieldEditor", "Field Panel - Configure Batch Fields", "BatchClass");
value.AddMenu("DocumentClass.FieldEditor", "Field Panel - Configure Index Fields", "DocumentClass");
value.AddMenu("CopyBatchFieldConfig", "Field Panel - Copy Batch Field Configuration", "BatchClass");
value.AddMenu("PasteBatchFieldConfig", "Field Panel - Paste Batch Field Configuration", "BatchClass");
value.AddMenu("CopyIndexFieldConfig", "Field Panel - Copy Index Field Configuration", "DocumentClass");
value.AddMenu("PasteIndexFieldConfig", "Field Panel - Paste Index Field Configuration", "DocumentClass");
Each entry is no identified by its event text, the first parameter. For example, BatchClass.GeneralConfig is intended to open up a generic configuration dialog - on batch class level.
Now, back to our ActionEvent - this is how I distinguish between the entry selected by the user:
if ((KfxOcxEvent)EventNumber == KfxOcxEvent.KfxOcxEventMenuClicked)
{
AdminForm form = new AdminForm();
switch ((string)Argument)
{
case "BatchClass.GeneralConfig":
ConfigureGeneral(kcApp.ActiveBatchClass);
break;
[I] want to call this setup form by clicking the Properties button in
the middle.
I don't know if you can use this button - I would assume yes - yet personally I tend to put settings either on batch or document class level. For example - your PDF annotation settings may different from document class to class - having an entry on this level seems more natural.
And what is the best way to store configured data and access it when
the runtime application gets executed?
Custom Storage Strings, and you can let your imagination run wild here. The most simplistic approach is to store key-value pairs during setup, and retrieve them in runtime. Here's a generic call (BatchClass is an IBatchClass object, i.e. a pointer to the ActiveBatchClass property of the AdminApplication object):
// set a CSS
BatchClass.set_CustomStorageString(name, value);
// get a CSS
BatchClass.get_CustomStorageString(name)
I usually use a single custom storage string only and store custom object - the object is a base64-encoded serialized XML using XmlSerializer - but again, that's up to you. The only recommendation is to rely on CSS only - don't use external files to store configuration parameters. A CSS is an integral part of your batch class - so, when exporting said class and importing it on a different system, your entire configuration will be there.
I need to think about how to store data because some users have user
profiles
Usually, you don't need to worry about that. The properties for user and password in ValidateUser are entirely optional - and since you're planning to write an unattended module - ideally a Windows Service, credentials should be maintained there. Kofax and Windows would automatically make sure the credentials are passed on, and your module will run under this user's context. Just make sure the user has permissions for the module and all associated batch classes. It's different if you're planning to write an attended module, for example an enhanced Validation module.
I want to create a custom Kofax module. When it comes to the batch processing the scanned documents get converted to PDF files. I want to fetch these PDF files, manipulate them (add a custom footer to the PDF document) and hand them back to Kofax.
So what I know so far:
create Kofax export scripts
add a custom module to Kofax
I have the APIRef.chm (Kofax.Capture.SDK.CustomModule) and the CMSplit as an example project. Unfortunately I struggle getting into it. Are there any resources out there showing step by step how to get into custom module development?
So I know that the IBatch interface represents one selected batch and the IBatchCollection represents the collection of all batches.
I would just like to know how to setup a "Hello World" example and could add my code to it and I think I don't even need a WinForms application because I only need to manipulate the PDF files and that's it...
Since I realized that your question was rather about how to create a custom module in general, allow me to add another answer. Start with a C# Console Application.
Add Required Assemblies
Below assemblies are required by a custom module. All of them reside in the KC's binaries folder (by default C:\Program Files (x86)\Kofax\CaptureSS\ServLib\Bin on a server).
Setup Part
Add a new User Control and Windows Form for setup. This is purely optional - a CM might not even have a setup form, but I'd recommend adding it regardless. The user control is the most important part, here - it will add the menu entry in KC Administration, and initialize the form itself:
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface ISetupForm
{
[DispId(1)]
AdminApplication Application { set; }
[DispId(2)]
void ActionEvent(int EventNumber, object Argument, out int Cancel);
}
[ClassInterface(ClassInterfaceType.None)]
[ProgId("Quipu.KC.CM.Setup")]
public class SetupUserControl : UserControl, ISetupForm
{
private AdminApplication adminApplication;
public AdminApplication Application
{
set
{
value.AddMenu("Quipu.KC.CM.Setup", "Quipu.KC.CM - Setup", "BatchClass");
adminApplication = value;
}
}
public void ActionEvent(int EventNumber, object Argument, out int Cancel)
{
Cancel = 0;
if ((KfxOcxEvent)EventNumber == KfxOcxEvent.KfxOcxEventMenuClicked && (string)Argument == "Quipu.KC.CM.Setup")
{
SetupForm form = new SetupForm();
form.ShowDialog(adminApplication.ActiveBatchClass);
}
}
}
Runtime Part
Since I started with a console application, I could go ahead and put all the logic into Program.cs. Note that is for demo-purposes only, and I would recommend adding specific classes and forms later on. The example below logs into Kofax Capture, grabs the next available batch, and just outputs its name.
class Program
{
static void Main(string[] args)
{
AppDomain.CurrentDomain.AssemblyResolve += (sender, eventArgs) => KcAssemblyResolver.Resolve(eventArgs);
Run(args);
return;
}
static void Run(string[] args)
{
// start processing here
// todo encapsulate this to a separate class!
// login to KC
var login = new Login();
login.EnableSecurityBoost = true;
login.Login();
login.ApplicationName = "Quipu.KC.CM";
login.Version = "1.0";
login.ValidateUser("Quipu.KC.CM.exe", false, "", "");
var session = login.RuntimeSession;
// todo add timer-based polling here (note: mutex!)
var activeBatch = session.NextBatchGet(login.ProcessID);
Console.WriteLine(activeBatch.Name);
activeBatch.BatchClose(
KfxDbState.KfxDbBatchReady,
KfxDbQueue.KfxDbQueueNext,
0,
"");
session.Dispose();
login.Logout();
}
}
Registering, COM-Visibility, and more
Registering a Custom Module is done via RegAsm.exe and ideally with the help of an AEX file. Here's an example - please refer to the documentation for more details and all available settings.
[Modules]
Minimal CM
[Minimal CM]
RuntimeProgram=Quipu/CM/Quipu.KC.CM/Quipu.KC.CM.exe
ModuleID=Quipu.KC.CM.exe
Description=Minimal Template for a Custom Module in C#
Version=1.0
SupportsTableFields=True
SupportsNonImageFiles=True
SetupProgram=Minimal CM Setup
[Setup Programs]
Minimal CM Setup
[Minimal CM Setup]
Visible=0
OCXFile=Quipu/CM/Quipu.KC.CM/Quipu.KC.CM.exe
ProgID=Quipu.KC.CM.Setup
Last but not least, make sure your assemblies are COM-visible:
I put up the entire code on GitHub, feel free to fork it. Hope it helps.
Kofax exposes a batch as an XML, and DBLite is basically a wrapper for said XML. The structure is explained in AcBatch.htm and AcDocs.htm (to be found under the CaptureSV directory). Here's the basic idea (just documents are shown):
AscentCaptureRuntime
Batch
Documents
Document
A single document has child elements itself such as pages, and multiple properties such as Confidence, FormTypeName, and PDFGenerationFileName. This is what you want. Here's how you would navigate down the document collection, storing the filename in a variable named pdfFileName:
IACDataElement runtime = activeBatch.ExtractRuntimeACDataElement(0);
IACDataElement batch = runtime.FindChildElementByName("Batch");
var documents = batch.FindChildElementByName("Documents").FindChildElementsByName("Document");
for (int i = 0; i < documents.Count; i++)
{
// 1-based index in kofax
var pdfFileName = documents[i + 1]["PDFGenerationFileName"];
}
Personally, I don't like this structure, so I created my own wrapper for their wrapper, but that's up to you.
With regard to the custom module itself, the sample shipped is already a decent start. Basically, you would have a basic form that shows up if the user launches the module manually - which is entirely optional if work happens in the back, preferably as Windows Service. I like to start with a console application, adding forms only when needed. Here, I would launch the form as follows, or start the service. Note that I have different branches in case the user wants to install my Custom Module as service:
else if (Environment.UserInteractive)
{
// run as module
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new RuntimeForm(args));
}
else
{
// run as service
ServiceBase.Run(new CustomModuleService());
}
}
The runtime for itself just logs you into Kofax Capture, registers event handlers, and processes batch by batch:
// login to KC
cm = new CustomModule();
cm.Login("", "");
// add progress event handlers
cm.BatchOpened += Cm_BatchOpened;
cm.BatchClosed += Cm_BatchClosed;
cm.DocumentOpened += Cm_DocumentOpened;
cm.DocumentClosed += Cm_DocumentClosed;
cm.ErrorOccured += Cm_ErrorOccured;
// process in background thread so that the form does not freeze
worker = new BackgroundWorker();
worker.DoWork += (s, a) => Process();
worker.RunWorkerAsync();
Then, your CM fetches the next batch. This can either make use of Kofax' Batch Notification Service, or be based on a timer. For the former, just handle the BatchAvailable event of the session object:
session.BatchAvailable += Session_BatchAvailable;
For the latter, define a timer - preferrably with a configurable polling interval:
pollTimer.Interval = pollIntervalSeconds * 1000;
pollTimer.Elapsed += PollTimer_Elapsed;
pollTimer.Enabled = true;
When the timer elapses, you could do the following:
private void PollTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
mutex.WaitOne();
ProcessBatches();
mutex.ReleaseMutex();
}
Previewing and Rejecting drafts using Contrib.Taxonomies in Orchard:
I have a requirement to preview (and if necessary, reject) taxonomy terms after an import.
I have attempted to do this with the following code:
A. I have changed the last line of the standard Import code to say:
Services.ContentManager.Create(term, VersionOptions.Draft);
(instead of VersionOptions.Published)
B. My preview screen has a "Publish" button, with a controller action with this code:
// remove current published terms
foreach (var term in _taxonomyService.GetTerms(taxonomyId)) {
_taxonomyService.DeleteTerm(term);
}
// publish draft terms created at import
foreach (var draftTerm in _MyTaxonomyService.GetDraftTerms(taxonomyId)) {
Services.ContentManager.Publish(draftTerm.As<ContentItem>());
}
C. The preview screen also has a "Reject" button, with a controller action with this code:
// delete drafts
foreach (var draftTerm in _MyTaxonomyService.GetDraftTerms(taxonomyId)) {
_taxonomyService.DeleteTerm(draftTerm);
}
With the above code, I can "Publish" as many times as I like and it works as expected.
However, if I "Reject" and then Publish I get "Permalinks in conflict" for the top-level term.
How can I remove the drafts without getting a permalinks conflict the next time I publish?
(Also note that removing drafts should not effect permalinks/display aliases of the current published terms).
Ok, the fundamental problem was that _taxonomyService.DeleteTerm() was firing the AutoRouteHandler.OnRemoved() event method.
This event method will call down the stack to AliasStorage.Remove(), which only takes path/aliassource as arguments, i.e. it will remove ALL AliasRecords that match the given path, i.e. aliases which the published version is still dependent on!
My solution to this problem was to replace the _taxonomyService.DeleteTerm() call with my own custom method _MyTaxonomyService.DeleteDraftTerm() for deleting draft terms. Note the removal of the AutoRoutePartHandler from the handler event calls:
public void DeleteDraftTerm(ContentItem contentItem)
{
var draftVersions = _contentItemVersionRepository.Fetch(x => x.ContentItemRecord == contentItem.Record && x.Latest && !x.Published);
var context = new RemoveContentContext(contentItem);
_handlers.Value.Invoke(handler => handler.Removing(context), Logger);
foreach (var version in draftVersions)
{
if (version.Latest)
{
version.Latest = false;
}
}
// comment this line out and replace with lines below
// _handlers.Value.Invoke(handler => handler.Removed(context), Logger);
var handlersExceptAutoRoute = _handlers.Value.Where(x => x.GetType() != typeof(AutoroutePartHandler));
handlersExceptAutoRoute.Invoke(handler => handler.Removed(context), Logger);
}
Tbh, I'm not really sure why this is necessary though. Before I added this new code, when I removed a draft content item with an autoroute part it seemed to result in there being no aliasrecords, but still having published autoroutepart records. So AutoRoutePartHandler.ProcessAlias() would think there was a duplicate published path and raise the conflict warning.
Hi guys and girls:) I have a question about parralelism and ms sql in C#
I have method that looks into Db for specific object. If it not exists it will add it to Db. Unfortunately it is done with Parallel.ForEach, so I have had some situation, using thread A and B:
A: look for entity with code 'xxx' - result: Not Exist
B: look for entity with code 'xxx' - result: Not Exist
A: Add entity to Db - result OK
B: Add entity to Db - result: "Violation of UNIQUE KEY constraint (...) The duplicate key value is 'xxx' "
what should I do to avoid that situation ?
For do not have this duplicate error, you want to catch here
try{
//execute you insert in base
}catch(Exception ex){
// If your constraint is not respected, an error is thrown.
console.WriteLine("db error : "+ex.Message);
}
But, it's temporary, It's functionnaly, but it's bad, it's not proper...
For have a proper code, you want to create a spooler:
class Spooler{
public System.Collections.Generic.List<String> RequestList = new System.Collections.Generic.List<String>();
public Spooler(){
// Open you Database
// Start you thread will be verify if a request adding in the collection
SpoolThread = new Thread(new ThreadStart(SpoolerRunner));
SpoolThread.Start();
}
public createRequestDb(String DbRequest){
RequestList.Add(DbRequest);
}
private void SpoolerRunner()
{
while (true)
{
if (RequestList.Count() >= 1){
Foreach (String request in RequestList){
// Here, you want to verify your request, if args already exist
// And add request in Database
}
}
// Verify is request exist in the collection every 30 seconds..
Thread.Sleep(30000);
}
}
}
Why using a spooler?
just because, when you initialize you spooler before call you threads, you want to call spooler in every threah, and, for each request, you adding request in collection, and, the spooler will process one after another... and not in the same time in every different threads...
EDIT:
This spooler is a sample, for insert a string request one by one in your database,
you can create a spooler with a collection for your object in you want, and insert in db if not exist... It's just a sample for a solution, when you have many threads, to have a treatments one after another ^^
Im using subsonic 2.2
I tried asking this question another way but didnt get the answer i was looking for.
Basically i ususally include validation at page level or in my code behind for my user controls or aspx pages. However i haev seen some small bits of info advising this can be done within partial classes generated from subsonic.
So my question is, where do i put these, are there particular events i add my validation / business logic into such as inserting, or updating. - If so, and validation isnt met, how do i stop the insert or update. And if anyone has a code example of how this looks it would be great to start me off.
Any info greatly appreciated.
First you should create a partial class for you DAL object you want to use.
In my project I have a folder Generated where the generated classes live in and I have another folder Extended.
Let's say you have a Subsonic generated class Product. Create a new file Product.cs in your Extended (or whatever) folder an create a partial class Product and ensure that the namespace matches the subsonic generated classes namespace.
namespace Your.Namespace.DAL
{
public partial class Product
{
}
}
Now you have the ability to extend the product class. The interesting part ist that subsonic offers some methods to override.
namespace Your.Namespace.DAL
{
public partial class Product
{
public override bool Validate()
{
ValidateColumnSettings();
if (string.IsNullOrEmpty(this.ProductName))
this.Errors.Add("ProductName cannot be empty");
return Errors.Count == 0;
}
// another way
protected override void BeforeValidate()
{
if (string.IsNullOrEmpty(this.ProductName))
throw new Exception("ProductName cannot be empty");
}
protected override void BeforeInsert()
{
this.ProductUUID = Guid.NewGuid().ToString();
}
protected override void BeforeUpdate()
{
this.Total = this.Net + this.Tax;
}
protected override void AfterCommit()
{
DB.Update<ProductSales>()
.Set(ProductSales.ProductName).EqualTo(this.ProductName)
.Where(ProductSales.ProductId).IsEqualTo(this.ProductId)
.Execute();
}
}
}
In response to Dan's question:
First, have a look here: http://github.com/subsonic/SubSonic-2.0/blob/master/SubSonic/ActiveRecord/ActiveRecord.cs
In this file lives the whole logic I showed in my other post.
Validate: Is called during Save(), if Validate() returns false an exception is thrown.
Get's only called if the Property ValidateWhenSaving (which is a constant so you have to recompile SubSonic to change it) is true (default)
BeforeValidate: Is called during Save() when ValidateWhenSaving is true. Does nothing by default
BeforeInsert: Is called during Save() if the record is new. Does nothing by default.
BeforeUpdate: Is called during Save() if the record is new. Does nothing by default.
AfterCommit: Is called after sucessfully inserting/updating a record. Does nothing by default.
In my Validate() example, I first let the default ValidatColumnSettings() method run, which will add errors like "Maximum String lenght exceeded for column ProductName" if product name is longer than the value defined in the database. Then I add another errorstring if ProductName is empty and return false if the overall error count is bigger than zero.
This will throw an exception during Save() so you can't store the record in the DB.
I would suggest you call Validate() yourself and if it returns false you display the elements of this.Errors at the bottom of the page (the easy way) or (more elegant) you create a Dictionary<string, string> where the key is the columnname and the value is the reason.
private Dictionary<string, string> CustomErrors = new Dictionary<string, string>
protected override bool Validate()
{
this.CustomErrors.Clear();
ValidateColumnSettings();
if (string.IsNullOrEmpty(this.ProductName))
this.CustomErrors.Add(this.Columns.ProductName, "cannot be empty");
if (this.UnitPrice < 0)
this.CustomErrors.Add(this.Columns.UnitPrice, "has to be 0 or bigger");
return this.CustomErrors.Count == 0 && Errors.Count == 0;
}
Then if Validate() returns false you can add the reason directly besides/below the right field in your webpage.
If Validate() returns true you can safely call Save() but keep in mind that Save() could throw other errors during persistance like "Dublicate Key ...";
Thanks for the response, but can you confirm this for me as im alittle confused, if your validating the column (ProductName) value within validate() or the beforevalidate() is string empty or NULL, doesnt this mean that the insert / update has already been actioned, as otherwise it wouldnt know that youve tried to insert or update a null value from the UI / aspx fields within the page to the column??
Also, within asp.net insert or updating events we use e.cancel = true to stop the insert update, if beforevalidate failes does it automatically stop the action to insert or update?
If this is the case, isnt it eaiser to add page level validation to stop the insert or update being fired in the first place.
I guess im alittle confused at the lifecyle for these methods and when they come into play