- CRM Integrated Customer Care Demo VPC released to partners. (CRM ICC VPC)
-
I have not posted much in the last month or so, as I have been busy working with several people on developing a customer service VPC to showcase Dynamics CRM and other Microsoft technologies. Now that it is complete and posted to Partner Source I can talk about it :)
The demo is called the CRM ICC Demo VPC. It shows a banking scenario built using Dynamics CRM and XRM at the core, an agent desktop shell, SharePoint for contact center portal and OCS for agent to agent communications. This demo shows how, using Microsoft Technologies, you can enable contact centers with a single view of your applications in an integrated agent desktop
Its important to note that this is a Solution in the sense that it is made of more then one product, it is targeted at 250 – 500+ Seat contact centers where there is a need to provide a unified UI, CTI, process automation across applications, case management and contact tracking.
You can Download it from here
*Note! you need to be a member of partner source to get to it and download it.
Sneak peak for folks that are interested …
This is a view of the CRM ICC demo while doing a Case create within CRM, there is a lot more to it from there, though.
The system leverages the XRM aspects of Dynamics CRM to do things like, high speed search on accounts and contacts, activity auditing, tracking calls, updating cases, navigating between things in CRM, handling email and a lot more. Additionally, we also demo some Microsoft services assets around task guidance and OCS.
Hope you guys enjoy it.
- Customer Care Framework 2009 sp1 QFE is released!
-
The CCF 2009 sp1 QFE was officially released and posted up on MSDN for folks that can download from there.
Their a number of updates in the QFE, You can read the summary here : http://blogs.msdn.com/ypitsch/archive/2009/08/25/ccf-2009-sp1-qfe.aspx
One of the most exciting bits in this release ( at least to me ) is the rollout of the Shell SDK for CCF. This SDK substantially simplifies the development and upkeep of applications developed for CCF, including allowing for the development of full WPF agent desktops.
Getting started with the Shell SDK in CCF is pretty simple, as part of the QFE package, 2 new sample implementations were provided that show a windows form desktop and a WFP desktop in use. They are a great place to start getting familiar with the new Shell SDK.
Folks that have built CCF desktops , or worked with them in the past you will notice a big difference between the Shell SDK and using the Core AIF API, I'm going to run though several of the key points here.
1) Core Desktop-
When building CCF desktops in the past, you have had to concern yourself with all sorts of things related to the CCF core support. Things like creating sessions, managing customer data in the context , CTI (computer telephony integration) manipulation and such. All of that work had to be done for each custom project because the business logic as interwoven to a degree with the way CCF rendered the desktop and worked with the components and hosted applications. Now, that capability didn't go away, in fact in some situations you need exactly that level of control to get the job done. You need a good deal of understanding of how the CCF desktop works in order to be effective here.
The Shell SDK changes this model, The Shell SDK is a layered SDK, built on AIF, that simplifies most of this work.. in comparison terms.. the Reference implementation of CCF 2009 sp1 ( including its support class’s ) have over 5K lines of code. Where as the sample implementation of the Windows Forms Desktop as part of the QFE samples has just over 750 lines of code, total and the WPF sample desktop has right around 420 lines of code.
You can see just from the line counts that something really changed, Where did all the extra stuff go?
That's where the Shell SDK comes in. The Shell SDK was built by fully separating the User Interface logic, the Rendering logic , and the plumbing of CCF from each other. This allows the Shell SDK to provide a unified support SDK to both windows forms and WPF based desktops… which means that if you learn the use of the Shell SDK, you can use it in a windows forms desktop or a WPF desktop without having to relearn the SDK.
Session Creation, Context Management, Customer Search, CTI management have all been moved into a compartmentalized model that allows you to manage them as host able elements of CCF rather than things that need to be thought about as you design the Desktop.
Another major feature here is that the Shell SDK provides a means to extend the desktop and add in your own specialized control interface to the CCF environment. For example, in working with Dynamics CRM, say you wanted one control to initialize the connection to the CRM API, then hand it to any other Hosted Control that needs to consume that connection, the Shell SDK makes that task simple and easy to do.
2) Sessions, Context and Search..
Context management and how Sessions are created was moved into a set of known interfaces that allow you to setup a search provider and a context manager and use them independently or pair them together. What does that really mean? well first off it means you can have multiple search providers running concurrently in the desktop shell. Second it means that you can associate each search provider to its own Context generator, So you can have 2 completely different entry points for create the initial session context.
How Sessions are created in the environment is also new, You no longer have direct access to the “AddSession” methods from the original CCF desktop code, however now you have an Interface that is used to identify a “Search Control”, that interface in turn allows you to raise a event called “CustomerSearchResult”. That event triggers the creation of a session in CCF.
Defining a customer, or data that describes what the session is all about has also changed. In the base AIF API, a Session is strongly typed customer object, in the Shell SDK the customer object is contained in a new class called “CcfCustomer”. The CcfCustomer class stores the customer data in a “object”, which can be consumed and cast into the correct customer data by a control that needs it. it also contains information regarding the search control that created it and the context object that is used to process it. The net of this is that we have a container to store customer data that can be used by controls that care, and ignored by controls that don't care.
You can take advantage of this a few different ways, let me outline one for you here..
Say you wanted to prepackage a search provider and lookup control set for your CRM solution ( in our case Dynamics CRM ). You can also package in the Context provider for that system, which means your prospective developer does not have to concern themselves with “how” the context is built. All they need to do is configure your controls into the CCF administration system and it “just works”.
Now, Say you also want to provide a custom search UI for another system, again you package all the bits together that you need, Configure it in CCF administration and it will appear, assuming you allow multiple search controls, in the desktop. When your end user uses the system, Searches that originate from a search control will go to the corresponding Context manager for processing.
Check out the Search Control example in the QFE Samples to see this in action.
3) CTI
The CTI system was pretty much completely rewritten for the QFE. Originally CTI was tightly bound to the Session logic and a known way of creating sessions, as well as specialized constructors for CTI adapters that wanted to process additional data.
In the Shell SDK, Much like the Search and Context Interfaces, A set of interfaces and prebuilt logic has been created to support CTI. There are a few key points to understand about the CTI System… First the term “CTI” in CCF applies to all types of channels. That means Voice, Chat, email, Video , SMS, whatever. Second, the CTI subsystem as all about call control for most call types, and includes media for things like Chat.
If your creating a CTI adapter, you now have a base class to support the “connector” which is what plugs into the CTI system itself. Business logic associated with the CTI system is implemented in a “desktop manager” that is paired up with the CTI solution. You also will extend a specific call state ( Call control ) manager and a Agent State Manager for the technology your using. The Core CTI system in CCF supports and encourages the idea that the connector layer should be separated from the BizLogic layer in the desktop. IE: if your using CTIVenderX version 4.0 and they update to version 5.1.2.333 then the only thing you need to touch ( most of the time :) ) is the connector.
Both the connector and the DesktopManager are also Hosted controls… so you can leverage the CCF System’s Role layer to allow you to load the appropriate CTI solution for whatever your agent needs Without having to change the code of the desktop or other components of the CCF desktop solution..
If your just writing a control to consume or visualize data from the CTI system, its as simple as consuming the CCF onboard CTI Consumer Interfaces, there are 2, one for call state and one for agent state, and then doing whatever you want. If you use the base types to build your component it will work across CTI vendors, or you can up cast the types to the Vendor specific ones and take advantage of any additional functionality you have added.
That's the Shell SDK in a nutshell :)
Lot of new and exciting stuff in there, plus a great way to start building out reusable components that can be “plugged In” to the Agent Desktop.
- Working with late loading Frames and Script in CCF
-
What the Heck is that?
When a Browser page that loads multiple levels of frames, or IFrames, with or without Java script attached to those frames is called up in IE, you will get Doc Complete alerts for each of the frames and IFrames loaded into the env.
The problem is that those Frame are not really loaded yet. they are still loading and take a few more cycles to get to a point where you can access them via the DOM. CCF’s WebDDA handles this for the most part however the web application adapters do not.
SO… how do you deal with it in a web application adapter.
The key to dealing with this sort of thing is waiting on the browser to get done loading and running all the initial scripts. in other words .. its all about timing. The catch is that IE doesn't give you a hint when its done, just the doc complete event which gets called when the page is done loading into the DOM, but not when the DOM is done processing the page.
The first thing you are likely to need to do is find the right frame in any given document that has what you are looking for. So to do that we need a few generic functions hunt tags for us.
The goal of these functions is to find the right document ( frame) that has what you are looking for. You don't have to search for what you need exactly, just something on the document you are looking for.
/// <summary>
/// Find the right HTML Doc Containing a given tag
/// </summary>
protected HTMLDocument GetDocumentContainingId(HTMLDocument doc, string id)
{
// Used to check to see if the first field in the search list has been found,
// and if so, what frame it was found in.
bool bFoundRightFrame = false;
// try to find the first Field,
// if the first field is not found. check for sub frames on the page
// if sub frames found check them for the field,
// if its found on a sub frame, set the focus to that frame.
if (doc.getElementById(id) == null)
{
if (doc.frames.length > 0)
{
doc = CheckForValue(doc, id, ref bFoundRightFrame);
}
}
else
{
// Field found.
// current document remains in focus.
bFoundRightFrame = true;
}
if (!bFoundRightFrame)
{
// didn't find the first tag name.. treat as failure and abort sign on process.
System.Diagnostics.Debug.WriteLine(
string.Format("Could not find the requested id in {0}", doc.url));
return null;
}
return doc;
}
The CheckForValue is a function I call repeatedly to drill though the sub docs.
/// <summary>
/// Check This frame for the tag.
/// </summary>
private static HTMLDocument CheckForValue(HTMLDocument doc, string id,
ref bool bFoundRightFrame)
{
for (int i = 0; i < doc.frames.length; i++)
{
if (bFoundRightFrame)
break;
object iFrameNum = i;
HTMLWindow2Class doc2 = (HTMLWindow2Class)doc.frames.item(ref iFrameNum);
// Check for the field I want in the subframe.
HTMLDocument doc3 = (HTMLDocument)doc2.document;
if (doc3.getElementById(id) != null)
{
// subDoc Found
doc = (HTMLDocument)doc2.document;
bFoundRightFrame = true;
return doc3;
}
if (doc3.frames.length > 0 )
return CheckForValue(doc3, id, ref bFoundRightFrame);
}
return doc;
}
Ok so between those methods we can find the doc’s that got something in it.
Now we need to add some new bits to help us deal with this.
What we are going to do here is to use the DocComplete event to trigger a Threaded Timer that will eventually call our method. We can do this a few times as necessary to deal with late loading. Using the code above to tell us when we can actually access it.
In our web application adapter add as a class var;
// Holds on to the next action to trigger
private Dictionary<string, string> NextActionList = null;
This var is used to hold onto a bit of data to tell us what we are looking for and what action to trigger next.
Ok, so we are going to trigger our initial command based on an Action, Lets say its a Navigate to http://mysite/pg/page1.aspx.
Page1.aspx has several frames and an IFrame, for example we are looking for something called “CreateNote” , which lives in a dynamically loaded sidebar. We also know ( using our DOM inspector tools ) that the sidebar that contains the “CreateNote” Object is called “CommandSideBar” and that its in an IFrame.
What we want to do is get to the “CreateNote” object and click it.
Our First action just does the navigate, and it adds in a hander to the NextActionsList
// In the Web Application Adapter DoAction Method...
if (action.Name.Equals("default", StringComparison.CurrentCultureIgnoreCase))
{
if ((ctxPointer != null) && (ctxPointer.Count > 0))
{
if ( !String.IsNullOrEmpty(ctxPointer["AccountID"]) )
{
// http://mysite/pg/page1.aspx is in the action.url
string sUrl = string.Format("{0}?oId={1}&oType=1&”+
”security=262167&tabSet=areaService", action.Url , ctxPointer["AccountID"]);
if ( NextActionList == null )
NextActionList = new Dictionary<string,string>();
NextActionList.Add("createnote" , "/page1.aspx");
Browser.Navigate(sUrl);
}
return false;
}
}
That will cause the Browser to head toward Page1.aspx.
In the Doc Complete we do this:
// Check to see if there are pending actions.
if (NextActionList != null)
{
if (NextActionList.Count > 0)
{
foreach (KeyValuePair<string,string> itm in NextActionList)
{
if (urlString.ToLower().Contains(itm.Value.ToLower()))
{
// found an item
NextActionList.Remove(itm.Key);
string sEmptyData = string.Empty;
AdapterFireRequestAction(new RequestActionEventArgs(this.Name,
"processthreadedaction", itm.Key));
}
}
}
}
Here we are looking at what came back to see if the page / URL is in the next Actions List.
if it is we then send an Action back to ourselves with the command as the data. This is simple and straight forward, however you can do some neat stuff with this.
Back in the Action Handler for the adapter.
if ( action.Name.Equals("processthreadedaction" ,
StringComparision.CurrentCultureIgnoreCase))
{
if ( action.data.Equals("createnote" ,
StringComparision.CurrentCultureIgnoreCase))
{
System.Threading.Timer tWebPusher = new System.Threading.Timer
(new System.Threading.TimerCallback(RunCreateNote), null,
TimeSpan.FromSeconds(1), TimeSpan.Zero);
return false;
}
}
Now this is sort neat.. We are using the System.Threading.Timer to create an AutoCallBack into a Method we specified without having to do a lot of other work. In this case we are telling it to wait 1 second, then call RunCreateNote.
RunCreateNote Looks like this:
private void RunCreateNote(object oData)
{
// just a pass though,
// you could get other bits from CCF here
// or add data and pass it into the process
CreateNoteProcess();
}
CreateNoteProcess is now where stuff really happens..
private delegate bool CreateNoteProcessDelg();
/// <summary>
/// Hits the Create Note Button if present
/// </summary>
private bool CreateNoteProcess()
{
// switch to main thread if necessary
if (Browser.InvokeRequired)
{
return (bool)Browser.Invoke(new CreateNoteProcessDelg(CreateNoteProcess));
}
else
{
HTMLDocument htmlDoc = Browser.Document as HTMLDocument;
htmlDoc = GetDocumentContainingId(htmlDoc, "CommandSideBar");
if (htmlDoc != null)
{
IHTMLElement crtNote = htmlDoc.getElementById("CreateNote");
if (crtNote != null)
{
IHTMLElementCollection col =
(IHTMLElementCollection)crtNote.children;
if (col != null)
{
foreach (IHTMLElement el in col)
{
if (el is IHTMLButtonElement)
{
el.click();
return true;
}
}
}
}
}
else
{
// Item not found
// You can Recall the Threaded Search again,
// or you can re add something to Next Action
}
}
return false;
}
so here we are making sure we are on the right thread to talk to the browser, Then calling the method “GetDocumentContainingId” which will give us the right frame / doc. Once we have that we execute the action we want to do. in this case we click a button.
If you don't find the root doc ( the frames not loaded or accessible yet ) you can re-fire the Timer Thread command we used to kick the Process off in the DoAction handler for processthreadaction.
This nets out to allowing you to handled a browser page that takes a while to load subpages or scripts. Iv also found that this works well for pages that embed Ajax that are on refresh timers in JavaScript.
Hopefully this will help you though handling these website types in CCF.
- Creating a CCF Customer Provider for Dynamics CRM
-
CCF includes a reference capability for retrieving customer data via server side request. This is provides an ability to access one or more data sources from a known access point within your network. This capability was developed to deal with the often stringent requirement that certain types of service calls, and data access must be done from a “known” or "Service” account. This set of bits over time has become known as the “Customer provider” or “Customer Service” depending on which CCF generation you started with :).
This feature set lets you define data sources and the structure of the data that is returned to CCF. This provider is run under a CCF service account, and can also access the CCF SSO system to request credentials for specific systems, based on the CCF service account.
The reference implementation that ships with CCF includes the full source code for the sample customer database.
If you installed the developer tools for CCF you will find this at %INSTALLDRIVE%\ Microsoft CCF 2009\Reference Implementations\AgentDesktop.sln
For this provider, we are going to modify the base example to talk to Dynamics CRM and Search for Accounts and Contacts in CRM 4.
What do you need to get started
A CCF 2009 server and client, with the developer tools installed
A Dynamics CRM 4 server installed and working
Dynamics CRM SDK
Visual Studio 2008
Getting Started
So First off, we need to set some rules down about what we are going to consider a “customer”. For this example a “customer” is an Account + Contact Combination where the Contact is pointing to an account.
For the sake of simplicity I’m going to start with a blank VS solution rather than opening the Reference project and weeding though to the bits we need.
Open Visual Studio and add an existing project:
%installdirectory%\Microsoft CCF 2009\Reference Implementations\Microsoft.Ccf\Csr\WebServices\Customer\Provider\Microsoft.Ccf.Csr.WebServices.Customer.Provider.csproj
Your project should look like this:
The CCF agent desktop talks to the customer WCF Service, which is installed in the Microsoft.Ccf.Csr.WebServices.Customer web site. The customer service provides a “provider” that allows CCF to query one or more search sources to identify a customer. The provider configuration is managed for the CCF deployment by the CCF Configuration system.
Modifying the CCF Customer Data contract
We need to define what our customer record for CCF is going to look like. The CCF customer record is a WCF Data Contract used by CCF to describe the “Customer” as such, the out of the box bits usually are not good enough.
Open up the CustomerProvider.cs File in Visual Studio and locate the region labeled Implementation helpers
Expand that region and you will see a class like :
/// <summary>
/// Sample customer record definition.
/// </summary>
[DataContract]
public class CustomerRecord
{ /// <summary>
/// The customer's ID
/// </summary>
[DataMember]
public string CustomerID;
This is the description of the data that CCF will get when a call to the customer service returns.
We need to make our first changes to this area.
We need to capture the Account ID and Contact ID associated with our CRM query as we will need those later when we are working with CRM inside CCF. We will also want to capture the Account Name and an Email address.
After the last entry in the CustomerRecord class ( should be PhoneMobile ) add:
[DataMember]
public Guid AccountID;
[DataMember]
public Guid ContactID;
[DataMember]
public string AccountName;
[DataMember]
public string EmailAddress;
Don’t forget the [DataMember] attribute. That is required by WCF so that it understands how to treat that value.
We are done with this file… this minor change will show up in CCF when we rebuilt the Customer service reference in the main projects. Close this file down.
Building the CRM Provider
Add a Class to our project and call it CRM4CustomerProvider.cs.
Your new class should look like this:
namespace Microsoft.Ccf.Csr.WebServices.Customer.Provider
{ public class CRM4CustomerProvider
{ }
}
Derive your class from Customer provider and Implement the abstract Class… you do that by adding “ : CustomerProvider “ to your class name. Once you do that you should get an auto complete prompt asking you to implement the abstract class, say Yes.
The updated class should now have number of members; it should be similar to this:
public class CRM4CustomerProvider : CustomerProvider
{ ...
public override CustomerProvider.CustomerRecord[]
GetCustomersByANI(string ani, int maxRecords)
{ throw new NotImplementedException();
}
...
}
There should be 8 total methods in the class now, all following the same pattern as you see above.
Each of these methods can be called from the CCF client to identify a customer. I am going to work just with the overridden method “GetCustomersByAni” for now.
Next we need to add a constructor to the class; CCF uses the provider constructors to send over relevant information from the CCF configuration system. In the case of the Customer Provider, it only sends a connection string. So even though we are not going to use it for this, we still need to handle it.
To do that, add this right after the class declaration
public CRM4CustomerProvider(string connectionstring)
: base(connectionstring)
{ }
Now we need to add in the CRM connectivity.
This is pretty simple to do for deployed CRM environments, and just a bit more complicated for CRM Online deployments. I am going to use a CRM Deployed example for now. CRM offers 2 ways to access the X/CRM API, The CRM SDK includes a number of helper library’s, functions and classes to aid in connecting and dealing with CRM data and the other is direct access to the X/CRM Web Service and the X/CRM Metadata Service. The web service offers strongly typed access to CRM data and types. So for this example we are going to use the web service API for X/CRM.
We need to add a reference to the X/CRM Web service API. You can find more info on setting up a connection to CRM Web Service here
In my case, I’m going to call it XRMSvc:
After adding the references, my project should look like this :
*note: for the demo I am showing, I’m going to hardcode some of the variables. In a “real” version, you would need to make things like user id ,pw and url configurable, the CCF configuration server is a good place for this, or the web.config file for the customer service.
We are going barrow some bits from the CRM SDK samples and create a few utility functions to help us with access to the X/CRM API.
We will use HTTP Caching to Cache the XRMSvc Connection. This is important, as the CRM service is a bit expensive, time wise, to setup the first time.
To do that, we need to add a reference to the project for System.Web so that we can access the HttpRuntime Class. This class will give us a number of common features for working with web services and web sites. We are going to use the static Cache class from this namespace to handle the cache for us. Once you have added the System.Web reference to the project, open the CRM4CustomerProvider.cs file and add a using statement to it.
At this point the using should look like this:
using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
We now need to add a method that will check to see if the service is in the server cache, and if not, we need to initialize the service and login.
Add this method to the class:
/// <summary>
/// try to get X/CRM Service from cache, if not ,
/// try to init a new instance and add it to cache.
/// </summary>
/// <returns></returns>
private XRMSvc.CrmService GetXrmSvc()
{ // Check cache for an instance of the service.
XRMSvc.CrmService XrmSvc =
(XRMSvc.CrmService)HttpRuntime.Cache["CRMCustomerProvider"];
if (XrmSvc == null)
{ // Didnt find a instance in cache,
// Intilize a new instance.
string orgName = "YOUR ORG HERE";
string serverUrl = "YOUR URL HERE";
XrmSvc = GetCrmService(serverUrl , orgName);
// Add it to cache.
HttpRuntime.Cache.Add(
"CRMCustomerProvider",
XrmSvc,
null,
System.Web.Caching.Cache.NoAbsoluteExpiration,
new TimeSpan(0, 20, 0),
System.Web.Caching.CacheItemPriority.Normal,
null);
}
// Return the service from cache.
return XrmSvc;
}
Replace “Your Org Here” with the name of your CRM org, and Server URL with the ip, or name : port of your server.
For example, if your CRM Web site is at http://contoso:5551/mscrm/… then the variables you would use here would be:
orgName = “mscrm”;
serverUrl = “http://contoso:5551”;
The X/CRM API also has discovery services that will figure out the proper org name for you, and, in the case of CRM Online, help you get the correct URL’s to connect to.
In the GetXrmSvc method, we are checking the server cache to see if we have an existing copy of the CRMCustomerProvider object, which is our X/CRM Service connection. If we have it, we exit right away, if we don’t, then we need to call the “GetCrmService()” method to set it up. When we get a response for that, we immediately set it into the cache for the next request.
You may notice the TimeSpan(0, 20, 0) in the cache command. That is telling the http cache to hold on to this for up to 20 min of inactivity, after someone touches it, reset the counter for another 20 min. This help us avoid having to rebuild the X/CRM API interface for each request.
Now we need to add the GetCrmService method, for that we are going to barrow a bit of the CRM SDK code. Specifically, I’m going to grab a method from the crmserviceutility.cs file that ships with the CRM SDK. If you have downloaded that SDK, you will find this class in %installpath%\sdk\server\howto\cs.
The method we want to use is called GetCrmService. Copy that method into the CRM4CustomerProvider.cs file after our “GetXrmSvc()” method call. Remove the ‘static’ tag from the GetCrmService method and use the Visual Studio auto complete to add the using statement to the file to reference the X/CRM Web Service.
When you’re done it should look like this:
/// <summary>
/// Set up the CRM Service.
/// </summary>
/// <param name="organizationName">My Organization</param>
/// <returns>CrmService configured with AD Authentication</returns>
public CrmService GetCrmService(string crmServerUrl, string organizationName)
{ // Get the CRM Users appointments
// Setup the Authentication Token
CrmAuthenticationToken token = new CrmAuthenticationToken();
token.OrganizationName = organizationName;
CrmService service = new CrmService();
if (crmServerUrl != null &&
crmServerUrl.Length > 0)
{ UriBuilder builder = new UriBuilder(crmServerUrl);
builder.Path = "//MSCRMServices//2007//CrmService.asmx";
service.Url = builder.Uri.ToString();
}
service.Credentials = System.Net.CredentialCache.DefaultCredentials;
service.CrmAuthenticationTokenValue = token;
return service;
}
One more thing to do with this method, the crmserviceutility.cs method uses “DefaultCredentials” for authenticating to the X/CRM system. As this a provider, it will inherit the identity of the IIS AppPool that it is running under. There are a number of ways to work this out, however for or example we are just going to update that and hardcode in a user account / pw to get it up and working.
Note, as I said above, this is for demo use only, for a “real” environment you need to handle this using AppPool identity or the CCF SSO system.
We need to update the line:
service.Credentials = System.Net.CredentialCache.DefaultCredentials;
to
service.Credentials = new System.Net.NetworkCredential("USER", "PW", "DOMAIN");
Putting your ID’s in for the user, PW and domain.
Now that all the housekeeping is done, we are ready implement the GetCustomersByAni method.
At the moment, that method looks like this:
public override CustomerProvider.CustomerRecord[]
GetCustomersByANI(string ani, int maxRecords)
{ throw new NotImplementedException();
}
The goal of this method is to identify a customer based on their phone number and return that customer’s information to CCF. This method is usually called from CCF’s lookup UI or by its AddSession methods. I say usually, because it can be called by developer code pretty much anywhere within the CCF environment.
The number that we are going to look for is handed to us in the ani field, MaxRecords is pretty self explanatory. So first off lets add some paranoia code in to check for the null’s, empty strings and failures.
public override CustomerProvider.CustomerRecord[]
GetCustomersByANI(string ani, int maxRecords)
{ if (string.IsNullOrEmpty(ani))
throw new ArgumentNullException("ani"); // No phone number provided
// Get the CRM Service Instance.
CrmService svc = GetXrmSvc();
if (svc == null)
throw new Exception("Unable to initialize CRM connection");
Now we are going to form the query. To do that we need to know a bit about what we are looking for in CRM. We are going to locate a contact by a phone number on the contact. CRM has several phone number associated with the contact out of the box, a quick look at the CRM metadata browser for the contact will give us what we need to know. You can find it here: http://<servername>:<port>/sdk/list.aspx .
In my case, it looks like this:
We can see 3 of the 4 phone number fields that we want to query against here, the other is “mobilephone”. We are going to guess that the number we are looking for is going to be in one of them. However we don’t necessary know the format that they are being stored in, we need to handle that as well.
One of things we have going for us here is that X/CRM’s query api accepts wild cards.. so we just need to do a bit of C# magic to parse out our string and build an appropriate query string.
We add this:
StringBuilder sTN = new StringBuilder();
sTN.Append("%");foreach (char c in ani)
{ if (Char.IsDigit(c))
{ sTN.Append(c);
sTN.Append("%"); }
else
{ if (c == '+')
sTN.Append(c);
}
}
ani = sTN.ToString();
What we are doing here is transforming a randomly formatted phone number into something that we can use to search regardless of the actual format in the CRM data system. So (123) 456-7890 turns into %1%2%3%4%5%6%7%8%9%0%. We can use this to search the CRM data system for, and get a hit, as long as it can find the numbers in order, regardless of the format.
We only need a few things out of the contact record at this point. And as you saw from the metadata browser there is a LOT of stuff in there, even before you add your own customizations to it. We need to create list of columns to return.
We add this:
ColumnSet cols = new ColumnSet();
cols.Attributes = new string[] { "contactid", "parentcustomerid", "firstname", "lastname", "address1_line1" , "address1_city" ,
"address1_stateorprovince" , "address1_postalcode" ,
"telephone1" , "telephone2" , "telephone3" , "mobilephone",
"emailaddress1"};
This identifies the bits that we are interested in returning from the query to X/CRM. There are number of better ways to do this :) however in the interests of simplicity, I show you the sledge hammer approach.
With our columns for the query of the contact record identified, we now need to build the filter. In X/CRM a filter is a collection of conditions. So we need to build a condition that covers each of our search fields.
We add this:
// Create Conditions
ConditionExpression condition_telephone1 = new ConditionExpression();
condition_telephone1.AttributeName = "telephone1";
condition_telephone1.Values = new string[] { ani };condition_telephone1.Operator = ConditionOperator.Like;
conditions.Add(condition_telephone1);
ConditionExpression condition_telephone2 = new ConditionExpression();
condition_telephone2.AttributeName = "telephone2";
condition_telephone2.Values = new string[] { ani };condition_telephone2.Operator = ConditionOperator.Like;
conditions.Add(condition_telephone2);
ConditionExpression condition_telephone3 = new ConditionExpression();
condition_telephone3.AttributeName = "telephone3";
condition_telephone3.Values = new string[] { ani };condition_telephone3.Operator = ConditionOperator.Like;
conditions.Add(condition_telephone3);
ConditionExpression condition_mobiletn = new ConditionExpression();
condition_mobiletn.AttributeName = "mobilephone";
condition_mobiletn.Values = new string[] { ani };condition_mobiletn.Operator = ConditionOperator.Like;
conditions.Add(condition_mobiletn);
Again, there are more efficient ways to do this, but it gets the job done.
We have the Conditions, now we need to add it to a filter.
We add:
FilterExpression filter = new FilterExpression();
filter.Conditions = conditions.ToArray(); // Our conditions
filter.FilterOperator = LogicalOperator.Or;
The OR is to allow for a hit on any of the conditions to return.
Now we tie it all together with an X/CRM Query command.
QueryExpression query = new QueryExpression();
query.EntityName = EntityName.contact.ToString();
query.ColumnSet = cols; // the cols we want.
query.Criteria = filter; // the filter
We are now ready to do make the query to X/CRM for the contact data. For that we use the RetrieveMultipleRequest class from the CRM SDK. That will allow us to bring back several records.
RetrieveMultipleRequest retrieve = new RetrieveMultipleRequest();
retrieve.Query = query;
We will catch the response from the query using the RetrieveMultipleResponse class from the CRM SDK. We are also going to define the List that will hold onto our customer records as we get them back from CRM. Note; the X/CRM API handles errors by throwing exceptions from its service, so we need to account for that as well.
List<CustomerRecord> results = new List<CustomerRecord>();
RetrieveMultipleResponse retrieved;
try
{ retrieved = (RetrieveMultipleResponse)svc.Execute(retrieve);
}
catch (SoapException ex1)
{ System.Diagnostics.Trace.WriteLine("CRM SOAP EXCEPTION : " + ex1.Detail.InnerText);
}
catch (Exception ex)
{ System.Diagnostics.Trace.WriteLine("CRM EXCEPTION : " + ex.Message);
}
The line that is actually placing the request is:
retrieved = (RetrieveMultipleResponse)svc.Execute(retrieve);
If successful, we will get a bit of data back answering our query request, if not, you will get a SOAP error from the X/CRM API telling you what it didn’t like.
Next we need to look at the response and parse it out for our data, because we can get multiple responses, we need a foreach loop.
if (retrieved != null)
{ foreach (BusinessEntity bizEnt in
retrieved.BusinessEntityCollection.BusinessEntities)
{ CustomerRecord cr = new CustomerRecord(); // result Record.
contact ct = (contact)bizEnt;
cr.City = ct.address1_city;
cr.ContactID = ct.contactid.Value;
cr.Country = ct.address1_country;
ct.contactid.Value.ToString();
cr.EmailAddress = ct.emailaddress1;
cr.FirstName = ct.firstname;
cr.LastName = ct.lastname;
cr.PhoneHome = ct.telephone2;
cr.PhoneMobile = ct.mobilephone;
cr.PhoneWork = ct.telephone1;
cr.State = ct.address1_stateorprovince;
cr.Street = ct.address1_line1;
cr.ZipCode = ct.address1_postalcode;
We have most of our Contact Record; however we still need a few more bits from the account. I’m just going to show you the code, as the method to get it is very similar to what we have done already with the important difference that I will use RetrieveRequest instead of RetrieveMultipleRequest. This form of request is “targeted” at a given X/CRM entity and ID. I have the ID of the related account from the ct.parentcustomerid field; this allows us to use this faster version of the query.
We add:
TargetRetrieveAccount tgAccount = new TargetRetrieveAccount();
tgAccount.EntityId = ct.parentcustomerid.Value;
ColumnSet Accountcols = new ColumnSet();
Accountcols.Attributes = new string[] { "accountid", "name" };
RetrieveRequest req = new RetrieveRequest();
req.ColumnSet = Accountcols; // Col's
req.Target = tgAccount; //what we are looking for.
RetrieveResponse resp;
try
{ resp = (RetrieveResponse)svc.Execute(req); // get the account
account ar = (account)resp.BusinessEntity; // Cast it.
cr.AccountID = ar.accountid.Value;
cr.AccountName = ar.name;
results.Add(cr); // add to the result list.
}
catch (SoapException ex1)
{ System.Diagnostics.Trace.WriteLine("CRM SOAP EXCEPTION : " + ex1.Detail.InnerText);
}
catch (Exception ex)
{ System.Diagnostics.Trace.WriteLine("CRM EXCEPTION : " + ex.Message);}
We are almost done with this method.
We just need to return our results now, so after the last catch closes.
return results.ToArray();
And we build.
If put together right, you should build without errors.
Testing your work
Now, before we register it with CCF, we should test it. Its quite a bit simpler to trouble shoot CCF providers outside the provider containers, as this allows us to focus in on the specific method if an issue is discovered. It also allows us to isolate any issues the correct area on CCF.
To do that, we are going to use a simple Winform with a button.
Add a Winform project to the solution and call it “CRMProviderTester”.
Your solution should now look like:
Add a project reference to the Microsoft.Ccf.Csr.WebServices.Customer.Provider project.
You will also need to add a file reference for Microsoft.Ccf.Common.Providers. you can find that file in the %INSTALLDRIVE%\ Microsoft CCF 2009\Framework folder.
Add a button to the form and implement the Click Event.
Next, Add the following to the using’s for the form;
using Microsoft.Ccf.Csr.WebServices.Customer.Provider;
In the click event handler, create an instance of our provider. You can pass in string.empty for the connection string as we are not using it. We also need to put in some code to invoke the GetCustomerByAni method
We add:
CRM4CustomerProvider provTest = new CRM4CustomerProvider(string.Empty);
CRM4CustomerProvider.CustomerRecord[] recs =
provTest.GetCustomersByANI("1234567890", 10);
if (recs != null)
{ MessageBox.Show(string.Format("there are {0} records" , recs.Length));}
We are ready to run our test program now, if it works, and you have a record in your system that matches the search request, you will get message box with the number of records returned. If not you will get a 0. If you get a 0 and you know your record to exist, look at the output window of VS for what’s wrong.
Most of the time the first issues are security related, the user account you connect to the service with will have the same privileges in X/CRM that you would, had you logged into the CRM web UI using that account. So if you cannot access it there, you will not be able to access it via the X/CRM API.
That wraps up this post. In the next post we will cover deploying the provider to the CCF configuration system, testing it, and then we will go though modifying CCF to accept our new Record and create a session for it.
- Customer Care Framework ( CCF ) and Dynamics CRM 4.0 – What's this all about?
-
As a Microsoft Technical Specialist focusing on customer care solutions, I often get asked questions regarding how Microsoft Customer Care Framework (CCF) and Microsoft Dynamics CRM work together, when they should be put together, and how they relate to one another. As a result, I thought it would be useful to share some of my insights, knowledge, and a bit of code with you.
Some of this may be background for you; but it’s probably best to start at the beginning.
Getting started
Usually, the first question in any CCF-CRM contact center implementation is, “Why do I need CCF to do this?” This is a good question and one you should think through carefully.
Microsoft Dynamics CRM, like most CRM implementations, is geared around being the center of your contact center solution. However, more often than not, your contact center staff needs to work with more than one application, launching several applications on their desktop at once, learning how to use them, and managing credentials across them. This common situation can get even more complicated when you want to use information provided by your computer telephony interaction (CTI) channel (like a telephone number or account number picked up by your interactive voice response [IVR] system) in more than one of the applications.
The Microsoft Customer Care Framework was initially developed by Microsoft Consulting Services to address exactly these challenges. CCF was productized in 2005 and has continued to be enhanced to further address these challenges across industries. Here are some of the features in CCF that aid in handling these challenges:
- CCF helps manage your interaction channels (including voice, e-mail, chat, and manual search) so they can provide information to any application CCF is currently hosting.
- CCF provides a search system that can span applications hosted in CCF or back-office applications exposed through services, databases, or other access technologies. The result of the search can then be provided to all applications in the CCF desktop.
- CCF enables you to manage what applications are displayed at any given time, which can be further influenced by a task or workflow-driven process. That means your staff will only see those applications or bits of applications that are required for a given task.
- CCF provides a single sign-on (SSO) system credential store to access and manage user credentials and inject those credentials right into applications. It can also support token-based authentication and other forms of sign-on.
- CCF enables you to create custom controls that can render data from service calls or applications running in the CCF desktop. For example, you can create a custom control to render a summary view of the customer or person you are working with and set this view as the first page when a customer record is opened.
- CCF enables you to automate common tasks across applications. For example, through the CCF user interface you can automate part of your ticketing process to record your contact with one ticket and create a separate ticket in another system to record the actual issue.
This list of examples illustrates why CCF makes sense when you have several applications and processes that your contact center needs to handle.
Microsoft Customer Care Framework 2009 has three major components
- Integrated Agent Desktop (IAD) smart client system
- Multichannel Engine
- Distributed Connectivity Services
The first two components have dependencies on the CCF Server while the third has dependencies on the Distributed Connectivity Services (DCS) Server; all three components also have dependencies on Microsoft SQL Server. For the purposes of this post, I will focus on the Integrated Agent Desktop component of CCF.
The Integrated Agent Desktop is designed to provide a composite application desktop supporting CCF controls (called a Hosted Control), Web applications, and legacy application support (which in this case means the Win32 applications and Java). It also allows the applications to exchange data and to use a central automation or workflow. Furthermore, it acts as an aggregation point for channels (meaning any form of contact whether CTI, chat, e-mail, or a person walking up to the counter) that are used to create sessions in CCF. For a more in-depth look at IAD, read though the CCF docs at http://msdn.microsoft.com/en-us/library/aa306213.aspx on AIF.
For more in-depth information on CCF, go to: www.microsoft.com/ccf
Microsoft Dynamics CRM 4 provides features focused through several different modules, those being:
- Service — customer care/ticketing and resource management
- Sales — sales force tools and solutions
- Marketing — campaigns and marketing management
For more in-depth information on Microsoft Dynamics CRM, go to: www.microsoft.com/crm
It’s important to understand that Microsoft Dynamics CRM is not only a CRM solution. It is really a line-of-business application platform that happens to ship with a fully-featured CRM solution built on top of it. This platform is known publicly as XRM. The CRM application user interface you see when you pull up a Microsoft Dynamics CRM Web site is a Microsoft .NET Web application built on top of XRM. However, for this blog post, I will refer to the overall system as CRM.
Why is that important?
It’s important because everything Microsoft Dynamics CRM does that affects data, including running workflows, importing, exporting, creating, or deleting, is actually a command to the underlying C/XRM API. This is key to creating a successful CCF-CRM integration, as you will find that some things are far simpler and faster to do using the underlying API than using the actual CRM UI.
This is really where the power of CCF and CRM come together. CCF is a smart client with the full power of .NET available to it, predesigned to support a composite application environment and a Windows Workflow-based system that allows for cross-application user interface automation. CRM provides the business engine and the base behaviors to make your solution incredibly powerful. With CCF you can use CRM to augment existing application features, or even use existing applications to augment the CRM feature set. Or you can base your complete solution around CRM and use CCF as the channel management engine and automate the CRM UI or data layer. At the same time you can manage the UI space through CCF to only show things that are necessary to the user.
So let’s take some common scenarios and walk though them
Let’s start with customer identification and handshake in a typical contact center setting. This is the process in which, on initial call, the customer is identified and the identity is confirmed via pass code or information verification.
For our case, let’s assume that the channel that initiates this request is the CTI channel, and that that channel is going to give us the ANI (the number of the caller), the DNIS (the number they called), and some properties that were added along the way by the automated call distributor (ACD). Also, we are going to assume that we have some sort of dialog box for the search interface.
How do we do this in CRM out of the box?
CRM provides a very powerful search tool that allows us to specify all sorts of parameters and filters when searching for a customer. So let’s start with the ANI. To find the customer in CRM we need to search across all the customer’s potential phone fields.
Or we can use the quick search.

Of the two approaches the quick search method is likely the more appropriate method for a contact center agent.
Note: various ACD vendors provide solutions that make customer lookup more direct with CRM. However, those solutions generally work for Microsoft Dynamics CRM only, and no other application on your desktop.
How would we do this same task in CCF?
In CCF we could take two different paths. The first would be to emulate the user behavior as we did above: navigate to the appropriate screen in CRM and push then push the data into it for searching. CCF provides several tools to be able to do this in a fairly straightforward manner. However, given that we are talking to CRM, this is really not an efficient way to do it. While CCF can do the work faster than the user, we still need to wait for the screens, acquire them, automate them, and then wait for the search to finish.
In a customer care desktop, the customer’s information keys (the telephone number, account number, or some similar combination of identifiers) are usually identified by the business ahead of time. For example, if the majority of our calls are existing customers that will identify themselves by, in order of precedence, an account ID (from the IVR), a phone number, an e-mail address, and finally a name, we can build a specialized UI that initially uses targeted searches for this information and then bubbles up to the CRM Advanced Search features if the user is not found.
It may make more sense to build this targeted search as a repeatable process inside CCF, accessing the C/XRM API directly. We can take advantage of knowing in advance where the data is inside the CRM data store and thus access it more quickly. The benefit is that the information retrieved by CRM in the initial lookup will be made available to all hosted applications and components inside CCF. The catch is that it does require a bit more up-front work to set up a CRM screen.
In our case the search dialog box looks like this:
And when we select the customer, his information appears in this detailed view:
It looks different then CRM, and all that CRM data is actually available to other the applications, visible and hidden, that are in the desktop.
I am going to cover the core of CCF / CRM interaction over the next series of posts.
I will cover:
- Customer Lookup Provider
- Customer Lookup UI and Creating a Session in CCF
- Displaying CRM Data using the CRM UI
- Creating a augment for CCF that will allow you to notes to CRM
- Creating a Ticket In CRM from CCF.
So, In the next post, I will explain how use the Customer Provider subsystem in CCF to build a Dynamics CRM Customer Lookup provider.
- 50K Concurrent Users! Who says CRM 4 can’t scale….
-
Over on the CRM Team Blog they have just published a scalability document for Microsoft Dynamics CRM 4.
The executive summary is available there and I have linked it here too.
- First time blogger.
-
Well, folks have been bugging me for a while to set this up and blog a bit, and I've decided to give it a go.
And Onward!