Writing "WriteLine" message to trace window in Windows Workflow 4.5 - c#-4.0

I am using rehosted-designer of WF 4.5 to give an interface for the users to create a workflow with custom activities and some of the inbuilt activities of the framework.
I am using AsyncCodeActivity model (perfect fit for my requirement), and hence is bound to execute a workflow as below, i mean execution is triggered at once without the possibility to iterate the activities in the workflow:
WorkflowApplication wf = new WorkflowApplication(activeFlowChart);
............
............
var result = wf.BeginRun(null,null);
while (!result.IsCompleted)
{
Thread.Sleep(1);
continue;
}
wf.EndRun(result);
My workflow can have multiple "WriteLine" activities scattered in the workflow.
I want to retrieve the message of a particular "WriteLine" and show in the trace window as in the order how it appears in the workflow.
I have tried as below on completion, which displays all the "WriteLine" messages appended at the end of the execution:
wf.Extensions.Add(writer);
wf.Completed = arg =>
{
if (!string.IsNullOrEmpty(writer.ToString()))
{
//display
}
};
I am looking for a way to get the "WriteLine" message immediately when it occurs in the workflow, not on completion.

The below link helped me to think about writing custom TextWriter to solve the above usecase:
Redirecting Console.WriteLine() to Textbox
Instead of appending the characters to the "writer" object and print it to the display window when the workflow is completed (refer the code in my question), i wrote a custom TextWriter to print the message of each "WriteLine" activity when a new line("\r\n") is encountered. example below:
in constructor:
var writer = new WriteLineTextWriter(DisplayTraceMessage);
Console.SetOut(writer);
custom TextWriter:
public class WriteLineTextWriter : TextWriter
{
private Action<string, Brush> WriteTraceMessageToGUI { get; set; }
private StringBuilder line;
private byte counter;
public WriteLineTextWriter(Action<string, Brush> action)
{
WriteTraceMessageToGUI = action;
line = new StringBuilder();
}
public override System.Text.Encoding Encoding
{
get { return System.Text.Encoding.Unicode; }
}
public override void Write(char value)
{
if (value == '\r' || value == '\n')
{
counter++;
}
else
{
line.Append(value.ToString());
}
if (counter == 2)
{
WriteTraceMessageToGUI(line.ToString(), Brushes.BlueViolet);
line.Clear();
counter = 0;
}
}
public override void Write(string value)
{
WriteTraceMessageToGUI(value, Brushes.BlueViolet);
}
}

Related

Release invoice on new screen

I need your help.
I have created a new screen, where I am calling all invoices pending release.
I have problems to release, I send a message where you request (you want to release).
It shows me the infinite message.
Only once should you ask me, then you should go out and follow the normal process.
public ProcessDocNew()
{
// Acuminator disable once PX1008 LongOperationDelegateSynchronousExecution [Justification]
Document.SetProcessDelegate(
delegate (List<ARInvoice> list)
{
List<ARRegister> newlist = new List<ARRegister>(list.Count);
foreach (ARInvoice doc in list)
{
newlist.Add(doc);
}
ProcessDoc(newlist, true);
}
);
Document.SetProcessCaption(ActionsMensje.Process);
Document.SetProcessAllCaption(ActionsMensje.ProcessAll);
}
public virtual void ProcessDoc(List<ARRegister> list, bool isMassProcess)
{
string title = "Test";
string sms = "¿Stamp?";
var Graph = PXGraph.CreateInstance<ARInvoiceEntry>();
ARInvoice document = Document.Current;
PEFEStampDocument timbrar = new PEFEStampDocument();/*This is a class where it is, all my method*/
if (isMassProcess == true)
{
Document.Ask(title, sms, MessageButtons.YesNo, MessageIcon.Question);
{
PXLongOperation.StartOperation(Graph, delegate
{
timbrar.Stamp(document, Graph); /*here I have my release method*/
});
}
}
}
public static class ActionsMensje
{
public const string Process = "Process";
public const string ProcessAll = "Process All";
}
I await your comments
Only once should you ask me, then you should go out and follow the
normal process.
That is not how the processing pattern works. The process delegate is called for each record and is therefore not a valid location to display a message that should be shown only once.
You would need to add a custom action to achieve that behavior. The scenario you're looking for should be implemented with a processing filter checkbox and processing filter to comply with best practices:
Documentation on processing screens implementation is available here:
https://help-2019r2.acumatica.com/Help?ScreenId=ShowWiki&pageid=a007b57b-af69-4c0f-9fd1-f5d98351035f

Running a long operation within an event handler

I need to run some address validation on Customer Location addresses using a 3rd party API to determine if the address is residential or commercial. This validation should run whenever an address field is changed. In other words, the validation should be run in the Address_RowUpdated event handler.
Because the function is calling a 3rd party API, I believe that it should be done in a separate thread, using PXLongOperation so that it does not hold up address saving and fails gracefully if the API is unavailable or returns an error.
However, I am not sure if the architecture of running a long operation within an event handler is supported or if a different approach would be better.
Here is my code.
public class CustomerLocationMaint_Extension : PXGraphExtension<CustomerLocationMaint>
{
protected virtual void Address_RowUpdated(PXCache sender, PXRowUpdatedEventArgs e)
{
PX.Objects.CR.Address row = (PX.Objects.CR.Address)e.Row;
if (row != null)
{
Location location = this.Base.Location.Current;
PXCache locationCache = Base.LocationCurrent.Cache;
PXLongOperation.StartOperation(Base, delegate
{
RunCheckResidential(location, locationCache);
});
this.Base.LocationCurrent.Cache.IsDirty = true;
}
}
protected void RunCheckResidential(Location location, PXCache locationCache)
{
string messages = "";
PX.Objects.CR.Address defAddress = PXSelect<PX.Objects.CR.Address,
Where<PX.Objects.CR.Address.addressID, Equal<Required<Location.defAddressID>>>>.Select(Base, location.DefAddressID);
FValidator validator = new FValidator();
AddressValidationReply reply = validator.Validate(defAddress);
AddressValidationResult result = reply.AddressResults[0];
bool isResidential = location.CResedential ?? false;
if (result.Classification == FClassificationType.RESIDENTIAL)
{
isResidential = true;
} else if (result.Classification == FClassificationType.BUSINESS)
{
isResidential = false;
} else
{
messages += "Residential classification is: " + result.Classification + "\r\n";
}
location.CResedential = isResidential;
locationCache.Update(location);
Base.LocationCurrent.Update(location);
Base.Actions.PressSave();
// Display relevant messages
if (reply.HighestSeverity == NotificationSeverityType.SUCCESS)
String addressCorrection = validator.AddressCompare(result.EffectiveAddress, defAddress);
if (!string.IsNullOrEmpty(addressCorrection))
messages += addressCorrection;
}
PXSetPropertyException message = new PXSetPropertyException(messages, PXErrorLevel.Warning);
PXLongOperation.SetCustomInfo(new LocationMessageDisplay(message));
//throw new PXOperationCompletedException(messages); // Shows message if you hover over the success checkmark, but you have to hover to see it so not ideal
}
public class LocationMessageDisplay : IPXCustomInfo
{
public void Complete(PXLongRunStatus status, PXGraph graph)
{
if (status == PXLongRunStatus.Completed && graph is CustomerLocationMaint)
{
((CustomerLocationMaint)graph).RowSelected.AddHandler<Location>((sender, e) =>
{
Location location = e.Row as Location;
if (location != null)
{
sender.RaiseExceptionHandling<Location.cResedential>(location, location.CResedential, _message);
}
});
}
}
private PXSetPropertyException _message;
public LocationMessageDisplay(PXSetPropertyException message)
{
_message = message;
}
}
}
UPDATE - New Approach
As suggested, this code now calls the LongOperation within the Persist method.
protected virtual void Address_RowUpdated(PXCache sender, PXRowUpdatedEventArgs e)
{
PX.Objects.CR.Address row = (PX.Objects.CR.Address)e.Row;
if (row != null)
{
Location location = Base.Location.Current;
LocationExt locationExt = PXCache<Location>.GetExtension<LocationExt>(location);
locationExt.UsrResidentialValidated = false;
Base.LocationCurrent.Cache.IsDirty = true;
}
}
public delegate void PersistDelegate();
[PXOverride]
public virtual void Persist(PersistDelegate baseMethod)
{
baseMethod();
var location = Base.Location.Current;
PXCache locationCache = Base.LocationCurrent.Cache;
LocationExt locationExt = PXCache<Location>.GetExtension<LocationExt>(location);
if (locationExt.UsrResidentialValidated == false)
{
PXLongOperation.StartOperation(Base, delegate
{
CheckResidential(location);
});
}
}
public void CheckResidential(Location location)
{
CustomerLocationMaint graph = PXGraph.CreateInstance<CustomerLocationMaint>();
graph.Clear();
graph.Location.Current = location;
LocationExt locationExt = location.GetExtension<LocationExt>();
locationExt.UsrResidentialValidated = true;
try
{
// Residential code using API (this will change the value of the location.CResedential field)
} catch (Exception e)
{
throw new PXOperationCompletedWithErrorException(e.Message);
}
graph.Location.Update(location);
graph.Persist();
}
PXLongOperation is meant to be used in the context of a PXAction callback. This is typically initiated by a menu item or button control, including built-in actions like Save.
It is an anti-pattern to use it anytime a value changes in the web page. It should be used only when a value is persisted (by Save action) or by another PXAction event handler. You should handle long running validation when user clicks on a button or menu item not when he changes the value.
For example, the built in Validate Address feature is run only when the user clicks on the Validate Address button and if validated requests are required it is also run in a Persist event called in the context of the Save action to cancel saving if validation fails.
This is done to ensure user expectation that a simple change in a form/grid value field doesn't incur a long validation wait time that would lead the user to believe the web page is unresponsive. When the user clicks on Save or a specific Action button it is deemed more reasonable to expect a longer wait time.
That being said, it is not recommended but possible to wrap your PXLongOperation call in a dummy Action and asynchronously click on the invisible Action button to get the long operation running in the proper context from any event handler (except Initialize):
using PX.Data;
using System.Collections;
namespace PX.Objects.SO
{
public class SOOrderEntry_Extension : PXGraphExtension<SOOrderEntry>
{
public PXAction<SOOrder> TestLongOperation;
[PXUIField(DisplayName = "Test Long Operation", Visible = false, Visibility = PXUIVisibility.Invisible)]
[PXButton]
public virtual IEnumerable testLongOperation(PXAdapter adapter)
{
PXLongOperation.StartOperation(Base, delegate ()
{
System.Threading.Thread.Sleep(2000);
Base.Document.Ask("Operation Done", MessageButtons.OK);
});
return adapter.Get();
}
public void SOOrder_OrderDesc_FieldUpdated(PXCache sender, PXFieldUpdatedEventArgs e)
{
if (!PXLongOperation.Exists(Base.UID))
{
// Calling Action Button asynchronously so it can run in the context of a PXAction callback
Base.Actions["TestLongOperation"].PressButton();
}
}
}
}

Modifying the GL Batch generated when a Bills and Adjustments document is released

The goal is taking the Journal Transaction generated from the AP Bill page and adding 2 additional rows in GLTran.
1st Attempt
First, I extended the Release action from the Journal Transactions graph to include the 2 new lines:
public class JournalEntryExt : PXGraphExtension<JournalEntry>
{
public delegate IEnumerable ReleaseDelegate(PXAdapter adapter);
[PXOverride]
public IEnumerable Release(PXAdapter adapter, ReleaseDelegate baseMethod)
{
baseMethod(adapter);
//new code
GLTran tranRow = new GLTran();
tranRow = this.Base.GLTranModuleBatNbr.Insert(tranRow);
tranRow.AccountID = 2713;
tranRow.SubID = 467;
tranRow.CuryDebitAmt = 100;
this.Base.GLTranModuleBatNbr.Update(tranRow);
tranRow = new GLTran();
tranRow = this.Base.GLTranModuleBatNbr.Insert(tranRow);
tranRow.AccountID = 1514;
tranRow.SubID = 467;
tranRow.CuryCreditAmt = 100;
this.Base.GLTranModuleBatNbr.Update(tranRow);
this.Base.Actions.PressSave();
return adapter.Get();
}
Result: Creating and releasing the batch, entered the 2 new lines correctly.
After this, I thought that releasing the AP Bill would also trigger this extended logic from the GL Page. However, that didn't occur - The release of the Bill doesn't seem to re-use the Release logic defined in the GL page.
2nd Attempt
Then, I went back to the GL page and included the logic in the RowPersisted event, so that the 2 new lines would get created right after saving the document:
public class JournalEntryExt : PXGraphExtension<JournalEntry>
{
protected virtual void Batch_RowPersisted(PXCache sender, PXRowPersistedEventArgs e)
{
if (e.Row == null)
{
return;
}
Batch batchRow = (Batch)e.Row;
if (batchRow != null
&& e.Operation == PXDBOperation.Insert
&& e.TranStatus == PXTranStatus.Completed)
{
////new code
GLTran tranRow = new GLTran();
tranRow = this.Base.GLTranModuleBatNbr.Insert(tranRow);
tranRow.AccountID = 2713;
tranRow.SubID = 467;
tranRow.CuryDebitAmt = 102;
this.Base.GLTranModuleBatNbr.Update(tranRow);
tranRow = new GLTran();
tranRow = this.Base.GLTranModuleBatNbr.Insert(tranRow);
tranRow.AccountID = 1514;
tranRow.SubID = 467;
tranRow.CuryCreditAmt = 102;
this.Base.GLTranModuleBatNbr.Update(tranRow);
}
}
Result: Creating and saving the Batch correctly entered the 2 new lines.
After this, I thought that releasing the AP Bill would trigger this extended event, given that a Journal Entry graph should get created and used from the Bill page, but in this case, also releasing the AP Bill, did not add the 2 new lines in the generated Batch.
3rd Attempt
Then I thought I could extend the Bill's Release action and take control of the generated Journal Entry with the Search<> method. However, in this case, the extended logic seems to be executed within a transaction as the Document.Current.BatchNbr was still NULL:
4th Attempt
Finally, I tried to extend the Persist() method of APReleaseProcess similarly to how it's done in the guide T300, however none of the methods are listed (version 17.207.0029):
Any other ideas as to how to enter these GL Lines?
Thanks!
Hopefully, it didn't take you forever to go through these 4 attempts... I've got to say though, the number of efforts and details in your question is quite impressive and definitely very appreciated!
A 2-step customization will be required to insert 2 additional GL Transactions in the Batch generated for an AP Bill:
to insert additional GL Transactions, you need to override Persist method within the JournalEntry BLC extension and invoke the logic to insert additional GLTrans only if the custom ModifyBatchFromAP boolean flag value equals True:
using PX.Data;
using System;
namespace PX.Objects.GL
{
public class JournalEntry_Extension : PXGraphExtension<JournalEntry>
{
private bool modifyBatchFromAP = false;
public bool ModifyBatchFromAP
{
get
{
return modifyBatchFromAP;
}
set
{
modifyBatchFromAP = value;
}
}
[PXOverride]
public void Persist(Action del)
{
if (ModifyBatchFromAP)
{
var glTran = Base.GLTranModuleBatNbr.Insert();
Base.GLTranModuleBatNbr.SetValueExt<GLTran.accountID>(glTran, "20000");
glTran = Base.GLTranModuleBatNbr.Update(glTran);
Base.GLTranModuleBatNbr.SetValueExt<GLTran.subID>(glTran, "000000");
glTran.CuryDebitAmt = 100;
glTran.TranDesc = "Additional Debit Transaction for AP Doc";
Base.GLTranModuleBatNbr.Update(glTran);
glTran = Base.GLTranModuleBatNbr.Insert();
Base.GLTranModuleBatNbr.SetValueExt<GLTran.accountID>(glTran, "20200");
glTran = Base.GLTranModuleBatNbr.Update(glTran);
Base.GLTranModuleBatNbr.SetValueExt<GLTran.subID>(glTran, "000000");
glTran.CuryCreditAmt = 100;
glTran.TranDesc = "Additional Credit Transaction for AP Doc";
Base.GLTranModuleBatNbr.Update(glTran);
}
del();
}
}
}
after that in the overridden Release action within the APInvoiceEntry_Extension, you will subscribe to the InstanceCreated event for the JournalEntry BLC type to set ModifyBatchFromAP flag value to True allowing your logic from step 1 to execute for the Batch generated only for an AP document:
using PX.Data;
using PX.Objects.GL;
using System.Collections;
namespace PX.Objects.AP
{
public class APInvoiceEntry_Extension : PXGraphExtension<APInvoiceEntry>
{
public delegate IEnumerable ReleaseDelegate(PXAdapter adapter);
[PXOverride]
public IEnumerable Release(PXAdapter adapter, ReleaseDelegate baseMethod)
{
PXGraph.InstanceCreated.AddHandler<JournalEntry>((JournalEntry graph) =>
{
graph.GetExtension<JournalEntry_Extension>().ModifyBatchFromAP = true;
});
return baseMethod(adapter);
}
}
}
P.S. it is not currently possible to use the Select Method to Override dialog with the APReleaseProcess class due to PXHiddenAttribute applied to it. Let me forward this to our Engineering Team to bring their attention on that matter.

How to pass parameters to a CodeActivity in a NativeActivity code sequence

I'm trying to get windows workflows working, and I've become a little stumped.
I've gotten a single workflow working, but now I am trying to do something a little more complex: start a workflow, where each activity itself contains a workflow. (Picture something like the main program starts the activities "Input, logic, and output", and then each of those have additional activities like "prompt user, get input, etc.")
I've had it working fine, with the example from here (http://msdn.microsoft.com/en-us/magazine/gg535667.aspx), when I am not passing any parameters from the main program to the activites. My question is, how exactly does the 'Variables' and 'metadata.SetVariablesCollection' work in the NativeActivity, and how to I get the parameters to the low level activities?
This is what I am currently trying:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Activities;
using System.Collections.ObjectModel;
using System.Activities.Statements;
namespace Project1
{
internal class MainProgram
{
internal static void Main(string[] args)
{
try
{
var act = new SimpleSequence();
act.Activities.Add((Activity)(new WriteSomeText()));
act.Activities.Add((Activity)(new WriteSomeText()));
act.Activities.Add((Activity)(new WriteSomeText()));
act.Variables.Add(new Variable<string> ("stringArg", "TEXT"));
WorkflowInvoker.Invoke(act);
}
catch (Exception ex)
{
System.Console.WriteLine("EXCEPTION: {0}", ex);
}
}
public class WriteSomeText : CodeActivity
{
[RequiredArgument]
public InArgument<string> stringArg { get; set; }
protected override void Execute(CodeActivityContext context)
{
string output = context.GetValue(stringArg);
System.Console.WriteLine(output);
}
}
public class SimpleSequence : NativeActivity
{
Collection<Activity> activities;
Collection<Variable> variables;
Variable<int> current = new Variable<int> { Default = 0 };
public Collection<Activity> Activities
{
get
{
if (this.activities == null)
this.activities = new Collection<Activity>();
return this.activities;
}
set
{
this.activities = value;
}
}
public Collection<Variable> Variables
{
get
{
if (this.variables == null)
this.variables = new Collection<Variable>();
return this.variables;
}
set
{
this.variables = value;
}
}
protected override void CacheMetadata(NativeActivityMetadata metadata)
{
metadata.SetChildrenCollection(this.activities);
metadata.SetVariablesCollection(this.variables);
metadata.AddImplementationVariable(this.current);
}
protected override void Execute(NativeActivityContext context)
{
if (this.Activities.Count > 0)
context.ScheduleActivity(this.Activities[0], onChildComplete);
}
void onChildComplete(NativeActivityContext context, ActivityInstance completed)
{
int currentExecutingActivity = this.current.Get(context);
int next = currentExecutingActivity + 1;
if (next < this.Activities.Count)
{
context.ScheduleActivity(this.Activities[next], this.onChildComplete);
this.current.Set(context, next);
}
}
}
}
}
This ends up throwing the following exception:
EXCEPTION: System.Activities.InvalidWorkflowException: The following errors were encountered while processing the workflow tree:
'WriteSomeText': Value for a required activity argument 'stringArg' was not supplied.
'WriteSomeText': Value for a required activity argument 'stringArg' was not supplied.
'WriteSomeText': Value for a required activity argument 'stringArg' was not supplied.
at System.Activities.Validation.ActivityValidationServices.ThrowIfViolationsExist(IList`1 validationErrors)
at System.Activities.Hosting.WorkflowInstance.ValidateWorkflow(WorkflowInstanceExtensionManager extensionManager)
at System.Activities.Hosting.WorkflowInstance.RegisterExtensionManager(WorkflowInstanceExtensionManager extensionManager)
at System.Activities.WorkflowApplication.EnsureInitialized()
at System.Activities.WorkflowApplication.RunInstance(WorkflowApplication instance)
at System.Activities.WorkflowApplication.Invoke(Activity activity, IDictionary`2 inputs, WorkflowInstanceExtensionManager extensions, TimeSpan timeout)
at System.Activities.WorkflowInvoker.Invoke(Activity workflow, TimeSpan timeout, WorkflowInstanceExtensionManager extensions)
at System.Activities.WorkflowInvoker.Invoke(Activity workflow)
at Project1.MainProgram.Main(String[] args) in c:\users\user\documents\visual studio 2010\Projects\ModelingProject1\Project1\MainProgram.cs:line 25
I know, I only pass 1 parameter, but the exception still says that I am missing 3 parameters. I am missing something as to how to do this properly.
You're correctly declaring stringArg as an InArgument but you're not passing any value to it when calling it inside SimpleSequence.
You can pass something using the constructor, while constructing the all activity itself, like this:
public class WriteSomeText : CodeActivity
{
[RequiredArgument]
public InArgument<string> stringArg { get; set; }
public WriteSomeText(string stringArg)
{
this.stringArg = stringArg;
}
protected override void Execute(CodeActivityContext context
{
string output = context.GetValue(stringArg);
System.Console.WriteLine(output);
}
}
// Calling the activity like this:
internal static void Main(string[] args)
{
var act = new SimpleSequence()
{
Activities =
{
new WriteSomeText("hello"),
new WriteSomeText("world"),
new WriteSomeText("!")
}
};
WorkflowInvoker.Invoke(act);
Console.WriteLine("Press any key to exit");
Console.ReadKey();
}
Also notice that is a best practice to use the constructor to initialize collections:
public SimpleSequence()
{
activities = new Collection<Activity>();
variables = new Collection<Variable>();
}
This way is even more intuitive to initialize the activity:
var act = new SimpleSequence()
{
Activities =
{
new WriteSomeText("hello"),
new WriteSomeText("world"),
new WriteSomeText("!")
},
Variables =
{
new Variable<int>("myNewIntVar", 10),
// ....
}
};
EDIT:
There are a couple of other ways to approach the problem. This is your best friend while starting in the WF4 world.
Check WF\Basic\CustomActivities\Code-Bodied for a little push with this particular case.

Retrieving values of ReadOnly fields from DynamicData DetailsView in Edit Mode on Updating using LinqDataSource

I have several tables in my database that have read-only fields that get set on Inserting and Updating, namely: AddDate (DateTime), AddUserName (string), LastModDate (DateTime), LastModUserName (string).
All of the tables that have these values have been set to inherit from the following interface:
public interface IUserTrackTable
{
string AddUserName { get; set; }
DateTime AddDate { get; set; }
string LastModUserName { get; set; }
DateTime LastModDate { get; set; }
}
As such, I have the following method on the Edit.aspx page:
protected void DetailsDataSource_Updating(object sender, LinqDataSourceUpdateEventArgs e)
{
IUserTrackTable newObject = e.NewObject as IUserTrackTable;
if (newObject != null)
{
newObject.LastModUserName = User.Identity.Name;
newObject.LastModDate = DateTime.Now;
}
}
However, by the time it hits this method, the e.OriginalObject has already lost the values for all four fields, so a ChangeConflictException gets thrown during the actual Update. I have tried adding the four column names to the DetailsView1.DataKeyNames array in the Init event handler:
protected void Page_Init(object sender, EventArgs e)
{
// other things happen before this
var readOnlyColumns = table.Columns.Where(c => c.Attributes.SingleOrDefaultOfType<ReadOnlyAttribute>(ReadOnlyAttribute.Default).IsReadOnly).Select(c => c.Name);
DetailsView1.DataKeyNames = DetailsView1.DataKeyNames.Union<string>(readOnlyColumns).ToArray<string>();
DetailsView1.RowsGenerator = new CustomFieldGenerator(table, PageTemplates.Edit, false);
// other things happen after this
}
I've tried making that code only happen on PostBack, and still nothing. I'm at a lose for how to get the values for all of the columns to make the round-trip.
The only thing the CustomFieldGenerator is handling the ReadOnlyAttribute, following the details on C# Bits.
UPDATE: After further investigation, the values make the round trip to the DetailsView_ItemUpdating event. All of the values are present in the e.OldValues dictionary. However, they are lost by the time it gets to the LinqDataSource_Updating event.
Obviously, there are the "solutions" of making those columns not participate in Concurrency Checks or other ways that involve hard-coding, but the ideal solution would dynamically add the appropriate information where needed so that this stays as a Dynamic solution.
i Drovani, I assume you want data auditing (see Steve Sheldon's A Method to Handle Audit Fields in LINQ to SQL), I would do this in the model in EF4 you can do it like this:
partial void OnContextCreated()
{
// Register the handler for the SavingChanges event.
this.SavingChanges += new EventHandler(context_SavingChanges);
}
private static void context_SavingChanges(object sender, EventArgs e)
{
// handle auditing
AuditingHelperUtility.ProcessAuditFields(objects.GetObjectStateEntries(EntityState.Added));
AuditingHelperUtility.ProcessAuditFields(objects.GetObjectStateEntries(EntityState.Modified), InsertMode: false);
}
internal static class AuditingHelperUtility
{
internal static void ProcessAuditFields(IEnumerable<Object> list, bool InsertMode = true)
{
foreach (var item in list)
{
IAuditable entity = item as IAuditable;
if (entity != null)
{
if (InsertMode)
{
entity.InsertedBy = GetUserId();
entity.InsertedOn = DateTime.Now;
}
entity.UpdatedBy = GetUserId();
entity.UpdatedOn = DateTime.Now;
}
}
}
}
Sadly this is not possible with EF v1

Resources