(re-)FireRequestAction on Web Hosted Applications

Apr 17, 2012 at 8:13 PM

Hi,

This is probably really simple, but I'm trying to build a custom HostedWPFControl that reads incidents via the CRM Services, displays them and then for each found incident provide a hyperlink where user is redirected to the incident in CRM:s web UI. The WPF-control is responsible of opening the incidents, I.e. no workflows, HAT etc. is involved in this.

Apart from the WPF-control, I have created one dynamic non-global Hosted Web Application which displays the actual incident. It is ok to just display one incident at a time, i.e. when the user clicks the another incident the content of the Hosted Web Application is to be redrawn with info about the other incident.

Problem is - when the user clicks the second incident the Web app is not refreshed without closing the dynamic app first.

My current setup:
Hosted web-app is a non-global dynamic application with
*) No adapter
*) User can close = true
*) about:blank as URL
Two user actions:
1) "default" - completely blank
2) "OpenIncident" where
   URL: [CRMSERVER]/main.aspx
   QS:  etc=112&id=%LastIncidentId&pagetype=entityrecord

 

LastIncidentId is a context variable I try to set prior to fire the actionrequest.

The HostedWPFControl implements the IDesktopUserActionsConsumer interface.

The solution below works - but I have to close the dynamic application before opening the incident:

 private void OpenIncident_Ugly(string incidentId)
        {
            string appName = "IncidentWebApp";
            string actionName = "OpenIncident";
            
            // set the context variable
            this.Context["LastIncidentId"] = incidentId;

            // Check if the app is already running
            if (_desktopAccess.AppExistsInUI(appName))
            {
                // Close it
                _desktopAccess.CloseDynamicApplication(appName);
            }
            // Start the application
            _desktopAccess.CreateDynamicApplication(appName);

            // Fire the requestaction            
            RequestActionEventArgs args = new RequestActionEventArgs(appName, actionName, "");
            FireRequestAction(args);

        }

But I would like the code below to work:

  private void OpenIncident_Better(string incidentId)
        {
            string appName = "IncidentWebApp";
            string actionName = "OpenIncident";

            // set the context variable
            this.Context["LastIncidentId"] = incidentId;

            // Check if the app is closed
            if (!_desktopAccess.AppExistsInUI(appName))
            {
                // Start the application
                _desktopAccess.CreateDynamicApplication(appName);
            }
            else
            {
                // Do nothing - use the already running application
            }
            
            // Fire the requestaction
            RequestActionEventArgs args = new RequestActionEventArgs(appName, actionName, "");
            FireRequestAction(args);

            // This does not work - the page is still the same (i.e. the first incident opened is still there)
        }

What have I missed? Do I manually have to notify the Web App that the context has changed so it re-reads the context variable LastIncidentId? Or are there any other mechanisms that are more suited for my scenarios?

Any help would be much appreciated.

BR,
Johan

Apr 24, 2012 at 8:24 PM

Great question,

The problem in your code is with how Context works.

Your using the Context to pass the LastIncidentId across,   if the LastIncidentId Does not exist in the context, this raises a context update. if it exists, it does not… therefor your target app ( IncidentWebApp ) does not get the new ID.

Now, assuming that your only using the LastIncidentId for the webapp nav behavior and nowhere else in CCA, the correct way to do this is to pass the GUID to the Action in the action data param.

So your correct block would be modified as such :

private void OpenIncident_Better(string incidentId)
    {
        string appName = "IncidentWebApp";
        string actionName = "OpenIncident";

        // set the context variable
        // this.Context["LastIncidentId"] = incidentId;

        // Check if the app is closed
        if (!_desktopAccess.AppExistsInUI(appName))
        {
            // Start the application
            _desktopAccess.CreateDynamicApplication(appName);
        }
        else
        {
            // Do nothing - use the already running application
        }
            
        // Fire the requestaction
        RequestActionEventArgs args = new RequestActionEventArgs(appName, actionName, incidentId);
        FireRequestAction(args);

    }

And you would then capture the ID in the DoAction Method  ( data param ) and pass it to the URL.

If you need to the LastIncidentId elsewhere then you need to force the context to update.. that would be done like this:

Context newCtx = new Context(this.Context.GetContext());
newCtx["LastIncidentId"] = incidentId;

ContextEventArgs ctxArgs = new ContextEventArgs(newCtx);
FireChangeContext(ctxArgs); 
// and if I want to self notify,
NotifyContextChange(newCtx);

Where the last line forces the control originating the request to update its local copy of the context too.

This causes all controls in the current session, except the control that originated the request, to signal the NotifyContext method

Hope that helps.

Mattb-msft.

Apr 27, 2012 at 10:52 PM

Hi,

Thank you Matt for your answer.

I'm still having trouble to get it work though.

If I understand you correctly, I cannot use the NotifyContextChange mechanism (the second code example in your reply) followed by a FireRequestAction to make the IncidentWebApp to refresh itself to the new URL? I tried, and it didn't work ;-) But as you said, the context was updated in all other apps.

I looked into your first suggestion (sending the incidentId as a data parameter), and made the assumption that the DoAction implementation is to be implemented in a WebAdapter (correct?). This is what I did:

public class IncidentWebAdapter : WebApplicationAdapter
    {        

        public override bool BeforeNavigate(ref string app, ref string action, ref string data, ref int flags, ref string headers, ref string url, ref object postData)
        {
            Debug.Print("IncidentWebAdapter.BeforeNavigate: action = '{0}' data = '{1}'", action, data);
            return base.BeforeNavigate(ref app, ref action, ref data, ref flags, ref headers, ref url, ref postData);
        }

       
        public override bool DoAction(HostedWebApplication.WebAction action, ref string data)
        {
            Debug.Print("IncidentWebAdapter.DoAction: WebAction.Name = '{0}' data = '{1}'", action.Name, data);

            string showIncidentActionName = "ShowIncident";

            if (action.Name == showIncidentActionName)
            {
                // The querystring is the same as during the attempt to use the context. 
                // We just replace the %LastIncidentId part with the incidentId
                string fullUrl = string.Format("{0}?{1}", action.Url, action.QueryString.Replace("%LastIncidentId", data));
                
                Browser.Navigate(fullUrl);

                return false; // To stop navigation in HostedWebApplication                
            }
            return true;
        }
}

It feels like the code above is pretty straight-forward, but I still can't make it work.

1) I found out that sometimes the default action was fired after the ShowIncident action (the first time the action is fired). Since the default action contains "about:blank" the result is a blank page (action sequence is "ShowIncident" --> "default" instead of the other way around). This is a behaviour that I also have experienced in my UII Workflows. I have implemented a HAT Workflow for authentication (using the SSO approach as in the Agent desktop sample implementation) in the default action and then run another action for the user-selected Workflow Step. If the selected Step´s action is called prior to the application's default action then it obviously doesn't work very well since the user hasn't been able to authenticate himself ;-)

2) The code above works perfectly the first time. But when I select another incident the DoAction method is executed in my adapter, but the BeforeNavigate method is never called (which is done at first call).

I really don't understand what causes this behaviour. Maybe it is because of the way I run the IE-processes. The only way integrated win authentication works in my environment is to run the CRM-pages in a security zone where Protected Mode is turned on. If I don't do this, nothing is shown in the agent desktop at all (i.e. the CRM app is not shown - this applies to all CRM pages, not only the incident page). Processes are shown in the task mgr though. Browsing to the CRM-pages in a "stand alone" IE Browser works perfectly though... (I asked around about this in the following post: http://crmcca.codeplex.com/discussions/231768 )

Any help would be much appreciated.

Thanks in advance,
Johan

Apr 28, 2012 at 12:12 AM

Your describing some very strange behavior,

IE Protected mode will prevent CCA from interacting properly with a web page.  So protected mode must be turned off for CCA to successfully interact with

I will spend a bit of time this weekend and implement this to show you what im talking about.

Do you want to see it as a HAT based Nav event or a web application adapter?

 

Mattb.

Apr 28, 2012 at 5:59 AM

Hi,

From my (very own) point of view I think I would like to see it as a web application adapter, even if the HAT based solution probably would be very useful as well ;-)

Many thanks,
Johan

 

Apr 30, 2012 at 5:02 AM

Ok, Iv posted a working version of what im talking about.

You can find it here

To set it up, you need to configure 2 applications, it is intended to be run as user session apps.
The CRMIncidentView is a hosted control configured as such:

Name : CRM Incident List
Type: HostedControl
Display Group: Mainpanel
Global : false
Assembly URI : CRMIncidentView
Assembly Type: CRMIncidentView.CRMIncidentViewCtrl

The second is a Web Application adapter.

Name: CRM Incident View
Type : Web Hosted Application
Display group: main panel
Global : false
Adapter: Use Adapter
URI : CRMIncidentView
Type : CRMIncidentView.WebAdapter
Dynamic : true
User Can Close : true
URL : about:blank

Action for CRM Incident View
Name : navtocase
URL: http://<YOURSERVERANDORG>/main.aspx
QueryString: etc=112&id=%UIIACTDATA&pagetype=entityrecord

Fundamentally, the activities are tied off the SelectionChanged event of the grid on the Cast List screen.
you can then follow the action though to the doAction behavior on the web app adapter.

Hope that helps, 
Mattb-msft.

 

Apr 30, 2012 at 8:28 AM

Matt,

Thank you very much - the queue/timer mechanism did the trick! The incident page is refreshed just as it should. I guess this kind of mechanism might be needed in the HAT nav scenario as well?

In parallell, I investigated why I have this "blank CRM pages issue": BIG SHAME ON ME (I should have looked into this before posting about it...)- I did not run/debug the CCA with sufficient permissions... When running the CCA with sufficient permissions I can open the pages without the protected mode option.

Again - thank you!

BR,
Johan

 

Apr 30, 2012 at 2:17 PM

The queue/ threading in the web app adapter is there to allow for IE to complete a long task ( running complex javascript for example ) before forcing it to navigate to another site.  This keeps IE happy, as it donset like to be yanked away from what it was doing.

The problem you were encountering likely had more to do with you invoke the web adapter as a dynamic application, send it a Navigate, then showing it.  IE Suspends processing when it’s out of view in UII by default, so by changing the order to “Invoke Dynamic app -> show dynamic app -> send action” , I think your original code would work.

Mattb.

Apr 30, 2012 at 10:52 PM
Matt,

We generate our session tabs almost 100% dynamically, I noticed in the code you specifically call out

uiiDesktopAccess.SetFocusOnApplication("x")  to get the default action to trigger, do you recommend that we do this everytime we instantiate an instace of a Dynamic Application?

For example, we have code like this:

function OpenDefaultSessionFromCTI(CustomerRecord customer,Guid CallRefId)
{
   //Not 100% exact, but this method is close
   OpenSession(customer,CallRefId);

   //Now we load our tabs based on the situation
   if(!uiiDesktopActions.AppExistsInUI("Customer"))
   {
       //I always assumed this would fire the default action
       success = uiiDesktopActions.CreateDynamicApplication("Customer");
   }

    if(!uiiDesktopActions.AppExistsInUI("Activity"))
    {
             success = uiiDesktopActions.CreateDynamicApplication("Activity");
           if(success)
           {
               RequestActionEventArgs _args = new RequestActionEventArgs("Activity","OpenInboundCall",data);
                FireRequestAction("Activity");
           }
     }
}
What happens is we run into situations where the "Activity" control presents the user with a white screen.  The form also has a lot of JScript that parses the URL I'm building with a web adapter based on the Session/Context data.  Hopefully this makes sense.

Thanks,

Jeremy

May 2, 2012 at 6:34 PM

So… first off, unless you have reason to do so, you should just let UII Open the session applications on its own, You should not call CreateDynamicApplications unless you actually need to access a Dynamic Application.

Now, if you have to do it this way, then you will need to use the SetFocus command after each DynamicApp create for web applications, this will allow IE to execute and get loaded properly.

As to your fire action at the end, again not real sure why you are doing that there as that’s part of the initial load of the environment, just let UII do the work for you and hook the context change event on the activity adapter, pick up your data, and set your initial state.

Mattb-msft.

May 3, 2012 at 2:11 PM

Thanks Matt.  Initially we had configured the Uii HOsted Applications to open with the session as it does out of the gate.  When we started getting into scenario's, there are a lot of cases where they don't need the other tabs in the context of the session.  It really depends on what support line is called by the customer or email address used to contact support.  So to really handle the situations they were looking for we had to really open specific apps for specific scenarios.

I will update the code to make sure I'm setting focus on my Dynamic Apps when we generate those.

As always your feedback is much appreciated.

Jeremy

May 3, 2012 at 2:32 PM
So, instead of doing it in the search control, I would recommend moving that logic over to a hidden hosted control and trigger your behavior on session created or the default context updated.

That gives you a level of isolation between controls and lets you manage Things a bit more cleanly. (testing and debugging wise )

That would also support the idea of opening an empty session and adding content to it as you learn more about the customer.

Mattb-MSFT
Sent from my Windows Phone

From: jwinchell
Sent: 5/3/2012 9:11 AM
To: mattb-msft@hotmail.com
Subject: Re: (re-)FireRequestAction on Web Hosted Applications [crmcca:352604]

From: jwinchell

Thanks Matt. Initially we had configured the Uii HOsted Applications to open with the session as it does out of the gate. When we started getting into scenario's, there are a lot of cases where they don't need the other tabs in the context of the session. It really depends on what support line is called by the customer or email address used to contact support. So to really handle the situations they were looking for we had to really open specific apps for specific scenarios.

I will update the code to make sure I'm setting focus on my Dynamic Apps when we generate those.

As always your feedback is much appreciated.

Jeremy

May 9, 2012 at 7:00 AM
Edited May 9, 2012 at 7:02 AM

Hello again,

When using the example code provided by Matt it works perfect for Browser.Navigate() scenarios, but it seems to be more trickier when configuring the web-app using the automation adapter (HAT).

I have a web-app using the automation adapter. Two actions:

1) default - is executed in a workflow assembly (it is a sign-in workflow so after automation of the "login"-button click it will navigate to another URL)

2) "MyAction" - another workflow assembly.

I want to invoke the web-app as a dynamic application and call "MyAction" on it. But, sometimes it seems like it starts "MyAction" before the default action. Or at least the default action isn't finished before it tries to start another action. I guess that this is a kind of threading issue (each workflow execution seems to be running on a separate thread).

I have tried to add an eventhandler to listen for the ActionCompletedEvent on the hosted web app (and from there call FireRequestAction("MyAction")), but it fires before the default action is finished with its automation activities.

Any suggestions?

Thanks in advance.

Johan

 

 

May 9, 2012 at 1:37 PM

Johan,

I am running into the same situation as you.  However, I'm using just Browser.Navigate in a Web Adapter with my Dynamic Applications.  I'm just about done implementing some of Matt's other suggestions above.  I will let you know if it solves the issue.

Have you thought about combining the sign-in workflow with the MyAction workflow?  Then you could make "MyAction" the default action for that control.

Jeremy

May 9, 2012 at 5:26 PM

Hi Jeremy,

I tried to combine the two actions but that made the workflow a bit "clumsy". And this approach wasn't painless either (see below). And, I really want to "reuse" the hosted application for different actions, and the sign-in process is just to be done at one time. Once user is authenticated, then (if I succeed with this) user can re-use the app for different kind of actions.

As mentioned, combining the two actions into one single workflow appeared to be harder than I first thought.

My scenario:

1) Workflow locates username and password box and then executes a control action on the login button (theres nothing wrong with the workflow. Running it stand-alone without trying to fire "MyAction" works like a charm), This is the default action of the hosted web app.

2) Once web-server has done authentication it redirects user to another page - the start-page for authenticated users

3) At this stage I want to run another action, i.e. "MyAction".

Problems I've had this far:
a) The default action seems to start before the entire login-page was loaded. Sometimes the workflow started but suddenly just quit. I investigated this and found out that the "ControlFinder" Activity sometimes threw an excetion. It was because all properties of the HAT-adapter werent initialized correctly when starting the workflow (got an exception with messages like "object reference not set..".). An ugly workaround was to run the ControlFinder activity within a while-loop and with a try-catch block around. And then use a Delay activity between each attempt to find the control. This worked but I really don't like the approach... NOTE: This behaviour is not true in all situations --> it happens from time to time...

b) Starting the dynamic app and then fire "MyAction" made the web-app to try to run "MyAction" before the default action was finished (as posted above) (not all the time, but very often anyway...)

c) Combining the two actions into one action gave me problems between step 2 and 3. In step 3, I cannot determine whether step 2 is finished. It directly tries to locate the control I want to Automate and this obviously doesn't work since the page not is loaded yet (i.e. page in step 2).

Regards,
Johan

 

 

May 14, 2012 at 3:04 AM

Pure HAT Based Adapters and WebApplication adapters actually operate at 2 different levels.

The Web application adapter is the low level interface, closer to the core if you will, whereas the HAT Automation adapter is a much higher level, construct.

As a general rule,

Default actions are called when the ADAPTER has loaded, not when whatever its doing is complete. While you cannot directly replicate what I did in the web application adapter in a HAT automation, you can approximate it by hooking the OnNavigated events from the page you’re listening too, and then trigger the behaviors you wish at that level.

 

Mattb.