locked
Using Silverlight (in CRM 2011), How do I block a form from closing until an async operation completes? RRS feed

  • Question

  • I'm using a silverlight piece in CRM 2011 to do some rich(er) advanced searching for related entities on an entity form.  I've used the HTML/JavaScript bridge to register a method to run when a user click's save as follows:

     

    dynamic Xrm = (ScriptObject)HtmlPage.Window.GetProperty("Xrm");
    Xrm..Page.data.entity.addOnSave(new Action(SaveHook));
    

     


    In my SaveHook method I need to add or remove a few entities based on what the user did inside my silverlight control. I'm managing this using DataServiceCollection objects linked to my OData context, then calling my context's BeginSaveChanges method with a save callback that simply calls EndSaveChanges.  The problem I'm having is that if the user click's Save & Close rather than save, it seems as though my callback never fires and the changes aren't committed.

    How do I block the form from closing until my save callback fires?


    • Edited by Nick Lindahl Monday, September 26, 2011 4:07 PM added CRM version
    Monday, September 26, 2011 4:04 PM

Answers

  • Ok, so I got my solution working.  First, I modified by Save hook registration to look like this:

    dynamic Xrm = (ScriptObject)HtmlPage.Window.GetProperty("Xrm");
    Xrm.Page.data.entity.addOnSave(new Action<dynamic>(SaveHook));
    


    Now my SaveHook method takes a dynamic parameter to contain the execution context of the form.  At the top of SaveHook, I added the following:

    var saveEventArgs = executionContext.getEventArgs();
    SaveMode saveMode = (SaveMode)(double)saveEventArgs.getSaveMode();
    


    SaveMode is an enumeration I created to map the values returned by getSaveMode() to something readable.  I found the values for the enumeration here.

    Next I run code to detect and set up any changes made via Silverlight, then the following code runs:

     if (somethingChanged)
    {
    	if (saveMode == SaveMode.SaveAndClose)
    		// temporarily cancel the save.
    		saveEventArgs.preventDefault();
    	_context.BeginSaveChanges(SaveCallback, new SaveContext() { SaveMode = saveMode, Context = _context });
    }
    


    The SaveContext type is something I set up so I can pass the context and the save mode back to the callback method (SaveCallback).  SaveCallback looks like this:

    private void SaveCallback(IAsyncResult result)
    {
    	Dispatcher.BeginInvoke(() =>
    		{
    			var ctx = result.AsyncState as SaveContext;
    			ctx.Context.EndSaveChanges(result);
    			if (ctx.SaveMode == SaveMode.SaveAndClose)
    				_xrm.Page.data.entity.save("saveandclose");
    		});
    }
    

    That re-fires the save event.  Nothing will have changed since the last Save fired, so the SaveHook method won't ever set the somethingChanged boolean variable which avoids the potential infinite loop.

    I think that's about it. Thanks again for your help, everybody.

    • Marked as answer by Nick Lindahl Monday, September 26, 2011 8:02 PM
    Monday, September 26, 2011 8:00 PM

All replies

  • Hi,

    1. You can add a new Two OptionSet field into the entity form i.e. is_silverlight_app_saving , drag it on form and hide it.

    2. On calling BeginSaveChanges method in silverlight application you may set the value of the is_silverlight_app_saving foeld to True

    3. In Form On Save event add a function that will check if the field  is_silverlight_app_saving has false or null value then only allow save form, you may also refer this post for information to cancel save event: http://worldofdynamics.blogspot.com/2011/08/dynamics-crm-2011-perform-jscript.html , can display alert message that currently changes are saving etc. try again in few minutes

    4. When the EndSaveChanges function invoked in Silvelright then change the value of the is_silverlight_app_saving field to False and can generate an alerts message that user can save record now or you can call the entity save method, i.e.

    Xrm.Page.data.entity.save( null | "saveandclose" |"saveandnew" )
     save() 
    If no parameter is included the record will simply be saved. This is the equivalent of the user clicking the Save button in the ribbon.
    save("saveandclose") 
    This is the equivalent of the user clicking the Save and Close button in the ribbon.
    save("saveandnew") 
    This is the equivalent of the user clicking the Save and New button in the ribbon.

    Jehanzeb Javeed

    http://worldofdynamics.blogspot.com
    Linked-In Profile |CodePlex Profile

    If you find this post helpful then please "Vote as Helpful" and "Mark As Answer".
    Monday, September 26, 2011 4:27 PM
  • Nick,

    You should do some reading into the possibility of the "onbeforeunload" event.  That said, it can be circumvented by a user--perhaps even innocently.  It is bad form, and unsupported for browser windows to prevent their own closure.  The best you can probably do is make it as inconvenient as you can... and for that, I'd recommend changing your asynchronous process into a synchronous one.  At least then, the thread running the window will be tied up until the script execution finishes, meaning that the user would have to forcibly close the window through the Task Manager or (if the script execution exceeds certain limits) the Windows "Application has stopped responding" dialog.


    Dave Berry - MVP Dynamics CRM - http:\\crmentropy.blogspot.com Please follow the forum guidelines when inquiring of the dedicated CRM community for assistance.
    Monday, September 26, 2011 6:03 PM
    Moderator
  • Here is a question:

    I had thought the async call in javascript or silverlight merely returns after the request is submitted to the service, and that it does not wait for the entire async plugin or workflow process to complete.  In that case Nick could not accomplish what he is intending without implementing some sort of polling operation on the entities involved to verify the changes were performed.

    By the way I agree that it is poor web ettiquete to force forms to remain open.  Your best bet may be to set the cursor to 'wait' and provide an 'Operation Complete' message at the end of the process.

    Monday, September 26, 2011 6:27 PM
  • Thanks for the suggestions everyone.  

    I also agree it is bad form to stop a window from closing when it really wants to.  :) I don't need to block the form from closing permanently.  Just until the async operation completes.  I also can't make the process synchronous without heavily modifying what I'm doing (remember I'm using Silverlight).

    I'm going to attempt something along the lines of what Jehanzeb suggested.  Before my async operation starts I'll cancel the save operation (temporarily) and kick it off again after my operation completes.  

    I'll post back with how it works out for me.

    Monday, September 26, 2011 7:16 PM
  • Ok, so I got my solution working.  First, I modified by Save hook registration to look like this:

    dynamic Xrm = (ScriptObject)HtmlPage.Window.GetProperty("Xrm");
    Xrm.Page.data.entity.addOnSave(new Action<dynamic>(SaveHook));
    


    Now my SaveHook method takes a dynamic parameter to contain the execution context of the form.  At the top of SaveHook, I added the following:

    var saveEventArgs = executionContext.getEventArgs();
    SaveMode saveMode = (SaveMode)(double)saveEventArgs.getSaveMode();
    


    SaveMode is an enumeration I created to map the values returned by getSaveMode() to something readable.  I found the values for the enumeration here.

    Next I run code to detect and set up any changes made via Silverlight, then the following code runs:

     if (somethingChanged)
    {
    	if (saveMode == SaveMode.SaveAndClose)
    		// temporarily cancel the save.
    		saveEventArgs.preventDefault();
    	_context.BeginSaveChanges(SaveCallback, new SaveContext() { SaveMode = saveMode, Context = _context });
    }
    


    The SaveContext type is something I set up so I can pass the context and the save mode back to the callback method (SaveCallback).  SaveCallback looks like this:

    private void SaveCallback(IAsyncResult result)
    {
    	Dispatcher.BeginInvoke(() =>
    		{
    			var ctx = result.AsyncState as SaveContext;
    			ctx.Context.EndSaveChanges(result);
    			if (ctx.SaveMode == SaveMode.SaveAndClose)
    				_xrm.Page.data.entity.save("saveandclose");
    		});
    }
    

    That re-fires the save event.  Nothing will have changed since the last Save fired, so the SaveHook method won't ever set the somethingChanged boolean variable which avoids the potential infinite loop.

    I think that's about it. Thanks again for your help, everybody.

    • Marked as answer by Nick Lindahl Monday, September 26, 2011 8:02 PM
    Monday, September 26, 2011 8:00 PM
  • Excellent work, Nick.  I'd love to see this process worked up as a blog post, or contributed to the CRM Wiki.
    Dave Berry - MVP Dynamics CRM - http:\\crmentropy.blogspot.com Please follow the forum guidelines when inquiring of the dedicated CRM community for assistance.
    Monday, September 26, 2011 8:55 PM
    Moderator
  • I still don't think the system archetecture supports what he really wants.  What I am seeing is that the async call only frees up the client.  The web service will spin off its own thread within the Async Service for the actual plugin/workflow execution and return an immediate response.  If the server process is lengthy enough, he will get a return value on the client before the process actually completes on the server.
    Monday, September 26, 2011 9:05 PM
  • JBlaeske,

    Exactly.  That is a great explanation of the problem I had, and the architecture is at the root of it.  What I did to solve it is cancel Entity Form's Save event before it gets sent to the server.  Then I fire my own save event for the related entities and wait for it to come back, then I re-fire the save for the form.  I step in and stop the out-of-the-box behaviour until my custom behaviour is done, then I start the OOB stuff over and make sure it skips my custom code on its second run.

    Oh, and as a side note, I had the same issue when the user clicked "Save and New" and handled it in a similar way.  

    Dave,  A Wiki entry is probably a good idea.  I'll try and find some time later this week to do that.


    --Nick
    Monday, September 26, 2011 9:17 PM
  • Hi Nick, Just added this to a heavy bit of silverlight customisation (a quote config tool) which was having trouble with the save and close, and it works an absolute charm. Serious thanks. Kevin
    Kevin Hughes
    Thursday, January 5, 2012 6:51 PM
  • Hi Nick,

    Can you please send me the complete code? It would greatly help me on a similar issue.

    My e-mail is: borsikadam@gmail.com

    Thanks in advance


    • Edited by Adam Borsik Thursday, May 10, 2012 11:43 AM
    • Proposed as answer by Phil98765 Thursday, July 12, 2012 9:51 PM
    • Unproposed as answer by Nick Lindahl Friday, July 13, 2012 2:13 PM
    Thursday, May 10, 2012 11:42 AM
  • BorsikAdam,

    Unfortunately, I don't think I'll be able to provide you with the complete code.  I can't simply give you the file as there is a lot of unrelated logic in there that may only confuse the issue for you.  Also, as the code isn't technically my property but that of my employer it is probably in my best interest not to distribute it too freely on the internet.  However, I (along with a great host of others in this community) would be happy to try to answer any questions you may have and provide you with whatever snippets you need to help you along your way.

    Which part of this solution would you like more detail on?


    --Nick

    Friday, May 11, 2012 2:42 PM
  • Hi Nick,

    What i would be interested in is your SaveHook method, because i cannot quite understand how to set it up, and how to pass the dynamic parameter which contains the execution context. I would also like some details about how your SaveContext class works.

    Thank you for your response

    Monday, May 14, 2012 9:06 AM
  • The SaveHook Signature looks like this:

    public void SaveHook(dynamic executionContext)
    {
       // implementation here
    }

    In order to use the 'dynamic' keyword you'll need a reference to Microsoft.CSharp

    To set the Silverlight control up to call SaveHook there are a few steps required in the UserControl_Loaded event handler.

    private void UserControl_Loaded(object sender, RoutedEventArgs e)
    {
    	dynamic _xrm = (ScriptObject)HtmlPage.Window.GetProperty("Xrm");
    
    	// register our save handler to be called when the CRM save buttons are clicked
    	_xrm.Page.data.entity.addOnSave(new Action<dynamic>(SaveHook));
    }

    The above code essentially registers a handler for a JavaScript save event emitted by the CRM form.  Silverlight wires it up to call your C# code when the user clicks one of the Save buttons.  The dynamic parameter is passed in automatically and will the the same context parameter you would receive if you were doing a JScript handler for the form's save event.

    The SaveContext Class I wrote doesn't do anything except group together a few variables I wanted to track between callbacks.  Here it is in its entirety:

    internal class SaveContext
    {
    	internal SaveMode SaveMode { get; set; }
    	internal OrganizationContext ODataContext { get; set; }
    }

    The SaveMode type is an enumeration which I mentioned in one of my posts above.  It looks like this:

    public enum SaveMode
    {
    	Save = 1,
    	SaveAndClose = 2,
    	SaveAndNew = 59,
    	Deactivate = 5,
    	Reactivate = 6
    }

    The source of numerical values were linked to in one of my posts above.

    The OrganizationContext type in the SaveContext class is just the auto-generated type from my reference to the OData source.

    Hopefully all that helps make it a little clearer.


    --Nick

    Monday, May 14, 2012 3:08 PM
  • Thank you very much for your help, this code works very nicely.
    Tuesday, May 15, 2012 8:37 AM