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();
}
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.
Suppose I want to pass one object (reference) through several pages. I can navigate and pass parameters via Frame.Navigate(typeof(FirstPage), object). But how to pass the reference back on back press properly?
protected override void OnNavigatedTo(NavigationEventArgs e) {
if (e.Parameter is SomeClass) {
this.someObject = (SomeClass)e.Parameter;
}
else {
this.someObject = new SomeClass();
}
SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility = AppViewBackButtonVisibility.Visible;
SystemNavigationManager.GetForCurrentView().BackRequested += OnHardwareButtonsBackPressed;
base.OnNavigatedTo(e);
}
private void OnHardwareButtonsBackPressed(object sender, BackRequestedEventArgs e) {
// This is the missing line!
Frame.Navigate(typeof(FirstPage), this.someObject);
}
But when I press back button it goes back to the FirstPage OnNavigatedTo with no parameter, and then back to the SecondPage OnHardwareButtonsBackPressed and then back to FirstPage OnNavigatedTo with filled parameter.
Could you please advice me some better approach?
In your back handler, don't navigate forwards again, just call GoBack -- and it's typically easier if you handle that at a global level rather than at a page level.
You can store your application state (the things you want to persist across page navigations) in global / static objects, or you could directly modify the object that was passed from the initial navigation (if the calling page still has a reference, it will be able to see the changes).
I would consider doing a search for "MVVM Windows Apps" and looking at some of the results to learn about a common way of building XAML apps.
Lets say I want a different main image for each page, situated above the page title. Also, I need to place page specific images in the left bar, and page specific text in the right bar. In the right and left bars, I also want layer specific content.
I can't see how I can achieve this without creating a layer for each and every page in the site, but then I end up with a glut of layers that only serve one page which seems too complex.
What am I missing?
If there is a way of doing this using Content parts, it would be great if you can point me at tutorials, blogs, videos to help get my head round the issue.
NOTE:
Sitefinity does this sort of thing well, but I find Orchard much simpler for creating module, as well as the fact that it is MVC which I find much easier.
Orchard is free, I understand (and appreciate) that. Just hoping that as the product evolves this kind of thing will be easier?
In other words, I'm hoping for the best of all worlds...
There is a feature in the works for 1.5 to make that easier, but in the meantime, you can already get this to work quite easily with just a little bit of code. You should first add the fields that you need to your content type. Then, you are going to send them to top-level layout zones using placement. Out of the box, placement only targets local content zones, but this is what we can work around with a bit of code by Pete Hurst, a.k.a. randompete. Here's the code:
ZoneProxyBehavior.cs:
=====================
using System;
using System.Collections.Generic;
using System.Linq;
using ClaySharp;
using ClaySharp.Behaviors;
using Orchard.Environment.Extensions;
namespace Downplay.Origami.ZoneProxy.Shapes {
[OrchardFeature("Downplay.Origami.ZoneProxy")]
public class ZoneProxyBehavior : ClayBehavior {
public IDictionary<string, Func<dynamic>> Proxies { get; set; }
public ZoneProxyBehavior(IDictionary<string, Func<dynamic>> proxies) {
Proxies = proxies;
}
public override object GetMember(Func<object> proceed, object self, string name) {
if (name == "Zones") {
return ClayActivator.CreateInstance(new IClayBehavior[] {
new InterfaceProxyBehavior(),
new ZonesProxyBehavior(()=>proceed(), Proxies, self)
});
}
// Otherwise proceed to other behaviours, including the original ZoneHoldingBehavior
return proceed();
}
public class ZonesProxyBehavior : ClayBehavior {
private readonly Func<dynamic> _zonesActivator;
private readonly IDictionary<string, Func<dynamic>> _proxies;
private object _parent;
public ZonesProxyBehavior(Func<dynamic> zonesActivator, IDictionary<string, Func<dynamic>> proxies, object self) {
_zonesActivator = zonesActivator;
_proxies = proxies;
_parent = self;
}
public override object GetIndex(Func<object> proceed, object self, IEnumerable<object> keys) {
var keyList = keys.ToList();
var count = keyList.Count();
if (count == 1) {
// Here's the new bit
var key = System.Convert.ToString(keyList.Single());
// Check for the proxy symbol
if (key.Contains("#")) {
// Find the proxy!
var split = key.Split('#');
// Access the proxy shape
return _proxies[split[0]]()
// Find the right zone on it
.Zones[split[1]];
}
// Otherwise, defer to the ZonesBehavior activator, which we made available
// This will always return a ZoneOnDemandBehavior for the local shape
return _zonesActivator()[key];
}
return proceed();
}
public override object GetMember(Func<object> proceed, object self, string name) {
// This is rarely called (shape.Zones.ZoneName - normally you'd just use shape.ZoneName)
// But we can handle it easily also by deference to the ZonesBehavior activator
return _zonesActivator()[name];
}
}
}
}
And:
ZoneShapes.cs:
==============
using System;
using System.Collections.Generic;
using Orchard.DisplayManagement.Descriptors;
using Orchard;
using Orchard.Environment.Extensions;
namespace Downplay.Origami.ZoneProxy.Shapes {
[OrchardFeature("Downplay.Origami.ZoneProxy")]
public class ZoneShapes : IShapeTableProvider {
private readonly IWorkContextAccessor _workContextAccessor;
public ZoneShapes(IWorkContextAccessor workContextAccessor) {
_workContextAccessor = workContextAccessor;
}
public void Discover(ShapeTableBuilder builder) {
builder.Describe("Content")
.OnCreating(creating => creating.Behaviors.Add(
new ZoneProxyBehavior(
new Dictionary<string, Func<dynamic>> { { "Layout", () => _workContextAccessor.GetContext().Layout } })));
}
}
}
With this, you will be able to address top-level layout zones using Layout# in front of the zone name you want to address, for example Layout#BeforeContent:1.
ADDENDUM:
I have used Bertrand Le Roy's code (make that Pete Hurst's code) and created a module with it, then added 3 content parts that are all copies of the bodypart in Core/Common.
In the same module I have created a ContentType and added my three custom ContentParts to it, plus autoroute and bodypart and tags, etc, everything to make it just like the Orchard Pages ContentType, only with more Parts, each with their own shape.
I have called my ContentType a View.
So you can now create pages for your site using Views. You then use the ZoneProxy to shunt the custom ContentPart shapes (Parts_MainImage, Parts_RightContent, Parts_LeftContent) into whatever Zones I need them in. And job done.
Not quite Sitefinity, but as Bill would say, Good enough.
The reason you have to create your own ContentParts that copy BodyPart instead of just using a TextField, is that all TextFields have the same Shape, so if you use ZoneProxy to place them, they all end up in the same Zone. Ie, you build the custom ContentParts JUST so that you get the Shapes. Cos it is the shapes that you place with the ZoneProxy code.
Once I have tested this, I will upload it as a module onto the Orchard Gallery. It will be called Wingspan.Views.
I am away on holiday until 12th June 2012, so don't expect it before the end of the month.
But essentially, with Pete Hurst's code, that is how I have solved my problem.
EDIT:
I could have got the same results by just creating the three content parts (LeftContent, RightContent, MainImage, etc), or whatever content parts are needed, and then adding them to the Page content type.
That way, you only add what is needed.
However, there is some advantage in having a standard ContentType that can be just used out of the box.
Using placement (Placement.info file) you could use the MainImage content part for a footer, for example. Ie, the names should probably be part 1, part 2, etc.
None of this would be necessary if there was a way of giving the shape produced by the TextField a custom name. That way, you could add as may TextFields as you liked, and then place them using the ZoneProxy code. I'm not sure if this would be possible.
Can anyone help me ,I want to customize upload functionality in which i want to validate the uploaded image type to the picture library
where can i set my script ?? Any one can advise ???
You might be Use ItemAdding. In ItemAdding Event Method just check extension of the Document before successfully uploaded to the Library.if unvalid document than through Error message
your code something like this :
protected string[] ValidExtensions = new string[] { "png", "jpeg", "gif"};
public override void ItemAdding(SPItemEventProperties properties)
{
string strFileExtension = Path.GetExtension(properties.AfterUrl);
bool isValidExtension = false;
string strValidFileTypes = string.Empty;
using (SPWeb web = properties.OpenWeb())
{
foreach (string strValidExt in ValidExtensions)
{
if (strFileExtension.ToLower().EndsWith(strValidExt.ToLower()))
{
isValidExtension = true;
}
strValidFileTypes += (string.IsNullOrEmpty(strValidFileTypes) ? "" : ", ") + strValidExt;
}
// Here i am going to check is this validate or not if not than redirect to the
//Error Message Page.
if (!isValidExtension)
{
properties.Status = SPEventReceiverStatus.CancelWithRedirectUrl;
properties.RedirectUrl = properties.WebUrl + "/_layouts/error.aspx?ErrorText=" + "Only " + strValidFileTypes + " extenstions are allowed";
}
}
}
You could use SPItemEventReceiver for your library and add your logic into ItemUpdating() and ItemAdding() methods.
You can try creating a custom list template and replace the default NewForm.aspx and EditForm.aspx pages there. These custom form templates need not contain the same user controls and buttons as in the default picture library template. You could create a Silverlight web part with rich UI to upload images, e.g. The more you want to differ the more code you'll have to write...
An OOTB solution I can think of would be a workflow that you would force every new picture to run through but it would be quite an overkill for the end-user...
Of course, if you're able to validate by using just the meta-data in ItemAdding as the others suggest, it'd be a huge time-saver.
--- Ferda
Here's another one for releasing objects created by reflection:
We're working with a reporting tool (Active Reports 6) which creates a dll for each report.
We have lots of clients that use similar but still unique reports.
Reports are read through a web interface.
We run multiple sites, one for each client.
Our choices are:
1) Put all the reports in one big project which will be called by all the sites.
Cost: It will need to be recompiled every time we make a small change to any one report, potentially creating problems for all sites.
2) Create a whole bunch of similar little projects, with one for each site - let's say for sake of space that this creates problems, too.
3) Create a "Report Factory" which will use reflection to wire-up report dlls as needed.
We chose "3".
Problem: The final product works fine except for one thing: It won't release the report dll when done.
There is not currently a problem with the operation within a test environment, but if you try to do anything in the folder with the report dlls, you get the following error message: "This action can't be completed because the folder or a file in it is open in another program"
After research on this site and others, we realized that we needed an AppDomain for each call which can be cleanly unloaded.
After still having problems, we realized that the AppDomainSetup object needed to have a setting that allowed it to optimize for multiple users (LoaderOptimization.MultiDomain)
That didn't work.
Unfortunately, the base object (Active 6 report) can not be serialized, so we can't make a deep copy and chuck the original object.
After doing all of this, we're still experiencing problems.
Here is the code (C#):
private object WireUpReport(ReportArgs args)
{
//The parameter 'args' is a custom type (ReportArgs) which merely contains a
name/value pair collection.
object myReport = null;
string sPath = String.Empty;
string sFriendlyName = String.Empty;
sFriendlyName = System.Guid.NewGuid().ToString();
Assembly asmReport = null;
AppDomainSetup ads = null;
AppDomain adWireUp = null;
ConstructorInfo ci = null;
Type myReportType = null;
Type[] parametypes = null;
object[] paramarray = null;
object retObject = null;
try
{
//Get Report Object
sPath = GetWireUpPath(args); //Gets the path to the required dll; kept in a config file
//This parameter is used in an overloaded constructor further down
ads = new AppDomainSetup();
ads.ApplicationBase = Path.GetDirectoryName(sPath);
ads.LoaderOptimization = LoaderOptimization.MultiDomain;
adWireUp = AppDomain.CreateDomain(sFriendlyName, AppDomain.CurrentDomain.Evidence, ads);
asmReport = adWireUp.GetAssemblies()[0];
asmReport = Assembly.LoadFrom(sPath);
//Create parameters for wireup
myReportType = asmReport.GetExportedTypes()[0];
parametypes = new Type[1];
parametypes[0] = typeof(ReportArgs);
ci = myReportType.GetConstructor(parametypes);
paramarray = new object[1];
paramarray[0] = args;
//Instantiate object
myReport = ci.Invoke(paramarray);
return myReport;
}
catch (Exception ex)
{
throw ex;
}
finally
{
//Make sure Assembly object is released.
if (adWireUp != null)
{
AppDomain.Unload(adWireUp);
}
if (asmReport != null)
{
asmReport = null;
}
if (ads != null)
{
ads = null;
}
if (adWireUp != null)
{
adWireUp = null;
}
if (ci != null)
{
ci = null;
}
if (myReportType != null)
{
myReportType = null;
}
if (parametypes != null)
{
parametypes = null;
}
if (paramarray != null)
{
paramarray = null;
}
}
}
The object which is returned from this code is cast as type ActiveReports and then passed around our application.
Any help would be deeply appreciated. Thanks
Your code looks like you are seriously misunderstanding how to interact with a separate AppDomain.
Think of communicating with an AppDomain like talking to someone who's currently in another country. You know where they are, but you can't just walk over and talk to them. If you want them to do something for you, you have to open up a line of communication and tell them what you need.
The way that you open that line of communication is by defining a proxy object that can be created inside the other AppDomain and then cross the boundary back to your current AppDomain. Being able to cross the boundary requires that your object either be marked as [Serializable] or inherit from MarshalByRefObject. Because we actually want to talk to a reference in the other AppDomain and not just have a copy of it, we need the proxy to do the latter.
private class CrossDomainQuery : MarshalByRefObject
{
public void LoadDataFromAssembly(string assemblyPath)
{
var assembly = Assembly.LoadFrom(assemblyPath);
//TODO: Do something with your assembly
}
}
There is a method on the AppDomain called CreateInstanceAndUnwrap() that will create an instance of that communication object inside the other AppDomain and then hand you back a __TransparentProxy object that can be cast to the proxy type.
var crossDomainQuery = (CrossDomainQuery)adWireUp.CreateInstanceAndUnwrap(
typeof(CrossDomainQuery).Assembly.FullName,
typeof(CrossDomainQuery).FullName);
Once you have that proxy object, you can call methods on it and they will be invoked in the other AppDomain.
crossDomainQuery.LoadDataFromAssembly(assemblyPath);
So how is this different from what your current example code is doing?
Your current code does not actually execute anything useful inside the other AppDomain.
adWireUp = AppDomain.CreateDomain(sFriendlyName, AppDomain.CurrentDomain.Evidence, ads);
asmReport = adWireUp.GetAssemblies()[0];
asmReport = Assembly.LoadFrom(sPath);
This creates a new AppDomain, but then it loads all of the assemblies from that AppDomain into your current AppDomain. Additionally, it explicitly loads your report assembly into your current AppDomain.
Creating an AppDomain and calling methods on it doesn't mean that your code is executing inside of it any more than reading about another country means that you're now talking to someone inside it.
Even if you do create a proxy object and execute code inside that other AppDomain, there are a few things to be aware of.
1) Both AppDomains must be able to see the type used for the proxy, and you may have to handle AssemblyResolve events for either AppDomain manually (at least temporarily) to help resolve that.
2) AppDomains are fairly expensive to create. Generally, they are not used in situations where you need to spin something up really quickly, take some action and disappear. You should plan on either keeping them around as long as you can or be prepared to take the performance hit on every call.
3) You've said that the report type that you're instantiating is not serializable, and being able to serialize the object is a requirement for passing that type back from the other AppDomain. Defining a serializable class that can transport relevant data across the boundary and using that to pass the report data might be an option, but you'll have to determine if that works for your particular situation.
Also, as an aside, unless you have logic that depends on variables being set to null, setting everything to null in your finally does nothing useful and complicates your code.