Monday, June 22, 2009

Personal Default Views Manager for Microsoft Dynamics CRM 4.0

The customer wants to make a personal setting for views according to next scenario.
Default account view is 'Active Accounts' for Tom, and 'My Active Accounts' view is default for Peter.

I've developed a little wizard and I would like to share it.


The following ideas for this wizard are:
1. Create a entity child to systemuser entity in which user can select default view for an entity.
2. To write plugin which will handle RetrieveMultiple message on savedquery entity.

1. Create a Default User View entity:
1.1. Creation of Default User View Entity:



Here I input Display Name, Plural Name, ownership is organization, name of entity. Mark all details as shown.



I set primary attribute name as new_entityname.



I create a relation between my newly created entity and systemuser entity where systemuser is primary entity.



I create required attributes:
new_defaultview - for default view identifier
new_defaultviewdisplayname - display name of default View
new_entitydisplayname - display name of entity (entity schema name will be stored in new_entityname field)
new_entitytypecode - entity type code

Appearance of form:



Second tab will be hide in OnLoad event handler - it contains fields Default View Name, Entity Display Name and Entity Type Code:



OnLoad Script of form:

//function converts text field Entity Name to picklist and fills it with entities
function ConvertEntityToPicklist(fieldName, dataItems)
{
var defaultValue = crmForm.all[fieldName].DataValue;
var table = crmForm.all[fieldName + "_d"];
var select = "<select req='0' id='" + fieldName + "' name='" + fieldName + "' defaultSelected='' class='ms-crm-SelectBox' tabindex='1170' onchange='new_entityname_onchange0();'>";
var defaultValueFound = false;

for (var i = 0; i < dataItems.length; i++)
if (dataItems[i].selectSingleNode('IsCustomizable').text == "true")
{
select += "<option value='" + dataItems[i].selectSingleNode('LogicalName').text + "' ";
select += "entitytypeid='"+dataItems[i].selectSingleNode('ObjectTypeCode').text+"'";
if (dataItems[i].selectSingleNode('LogicalName').text == defaultValue)
{
select += " SELECTED";
defaultValueFound = true;
}
select += ">" + dataItems[i].selectSingleNode('DisplayName/LocLabels/LocLabel/Label').text + "</option>";
}

if ((defaultValue != null) && (defaultValue.length > 0) && !defaultValueFound)
{
select += "<option value='" + defaultValue + "' SELECTED>" + defaultValue + "</option>";
}

select += "</select>";
table.innerHTML = select;
}

//function converts text field Default View to picklist and fills it with Views for entity selected in Entity Name picklist
function CreateQueriesList(fieldName, dataItems)
{
var defaultValue = crmForm.all[fieldName].DataValue;
var table = crmForm.all[fieldName + "_d"];
var select = "<select req='0' id='" + fieldName + "' name='" + fieldName + "' defaultSelected='' class='ms-crm-SelectBox' tabindex='1170'>";
var defaultValueFound = false;

for (var i = 0; i < dataItems.length; i++)
{
select += "<option value='" + dataItems[i].selectSingleNode('./savedqueryid').nodeTypedValue + "' ";
if (dataItems[i].selectSingleNode('./savedqueryid').nodeTypedValue == defaultValue)
{
select += " SELECTED";
defaultValueFound = true;
}
select += ">" + dataItems[i].selectSingleNode('./name').nodeTypedValue + "</option>";
}

if ((defaultValue != null) && (defaultValue.length > 0) && !defaultValueFound)
{
select += "<option value='" + defaultValue + "' SELECTED>" + defaultValue + "</option>";
}

select += "</select>";
table.innerHTML = select;
}

//Hide second tab
crmForm.all.tab1Tab.style.display = 'none';

//Request to retrieve all entities
var request = "" +
"<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">" +
GenerateAuthenticationHeader() +
" <soap:Body>" +
" <Execute xmlns=\"http://schemas.microsoft.com/crm/2007/WebServices\">" +
" <Request xsi:type=\"RetrieveAllEntitiesRequest\">" +
" <RetrieveAsIfPublished>true</RetrieveAsIfPublished>" +
" <MetadataItems>EntitiesOnly</MetadataItems>" +
" </Request>" +
" </Execute>" +
" </soap:Body>" +
"</soap:Envelope>";

var xmlHttpRequest = new ActiveXObject("Msxml2.XMLHTTP");

xmlHttpRequest.Open("POST", "/mscrmservices/2007/MetadataService.asmx", false);
xmlHttpRequest.setRequestHeader("SOAPAction","http://schemas.microsoft.com/crm/2007/WebServices/Execute");
xmlHttpRequest.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
xmlHttpRequest.setRequestHeader("Content-Length", request.length);
xmlHttpRequest.send(request);

var result = xmlHttpRequest.responseXML;

//Retrieve a collection of entities
var schemaNames = result.selectNodes("//CrmMetadata/CrmMetadata");

//Convertion
ConvertEntityToPicklist('new_entityname', schemaNames);

//Request to retrieve all Public Views for entity
var xml = "<?xml version='1.0' encoding='utf-8'?>"+
"<soap:Envelope xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'"+
" xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'"+
" xmlns:xsd='http://www.w3.org/2001/XMLSchema'>"+
GenerateAuthenticationHeader()+
"<soap:Body>"+
"<Fetch xmlns='http://schemas.microsoft.com/crm/2007/WebServices'>"+
"<fetchXml>"+
"&lt;fetch mapping='logical'&gt;"+
" &lt;entity name='savedquery'&gt;"+
" &lt;attribute name='name'/&gt;"+
" &lt;attribute name='savedqueryid'/&gt;"+
" &lt;filter type='and'&gt;"+
" &lt;condition attribute='isprivate' operator='eq' value='false'/&gt;"+
" &lt;condition attribute='inproduction' operator='eq' value='true'/&gt;"+
" &lt;condition attribute='isquickfindquery' operator='eq' value='false'/&gt;"+
" &lt;condition attribute='returnedtypecode' operator='eq' value='"+
document.getElementById('new_entityname').options[document.getElementById('new_entityname').selectedIndex].getAttribute('entitytypeid')+
"'/&gt;"+
" &lt;condition attribute='querytype' operator='eq' value='0'/&gt;"+
" &lt;/filter&gt;"+
" &lt;/entity&gt;"+
"&lt;/fetch&gt;"+
"</fetchXml>"+
"</Fetch>"+
"</soap:Body>"+
"</soap:Envelope>";

var xHReq = new ActiveXObject("Msxml2.XMLHTTP");
xHReq.Open("POST", "/mscrmservices/2007/CrmService.asmx", false);
xHReq.setRequestHeader("SOAPAction","http://schemas.microsoft.com/crm/2007/WebServices/Fetch");
xHReq.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
xHReq.setRequestHeader("Content-Length", xml.length);
xHReq.send(xml);
var resultXml = xHReq.responseXML;
var resultSet = resultXml.text;
resultSet.replace('&lt;','<');
resultSet.replace('&gt;','>');

var oXmlDoc = new ActiveXObject("Microsoft.XMLDOM");
oXmlDoc.async = false;
oXmlDoc.loadXML(resultSet);

//Retrieve a collection of Views
var results = oXmlDoc.getElementsByTagName('result');

//Conversion
CreateQueriesList('new_defaultview', results);


OnSave script of Form:

crmForm.all.new_entitydisplayname.DataValue = document.getElementById('new_entityname').options[document.getElementById('new_entityname').selectedIndex].innerText;

crmForm.all.new_defaultviewdisplayname.DataValue = document.getElementById('new_defaultview').options[document.getElementById('new_defaultview').selectedIndex].innerText;

crmForm.all.new_entitytypecode.DataValue = document.getElementById('new_entityname').options[document.getElementById('new_entityname').selectedIndex].getAttribute('entitytypeid');


Entity Name OnChange event handler:

//Request to retrieve all Public Views for entity
var xml = "<?xml version='1.0' encoding='utf-8'?>"+
"<soap:Envelope xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'"+
" xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'"+
" xmlns:xsd='http://www.w3.org/2001/XMLSchema'>"+
GenerateAuthenticationHeader()+
"<soap:Body>"+
"<Fetch xmlns='http://schemas.microsoft.com/crm/2007/WebServices'>"+
"<fetchXml>"+
"&lt;fetch mapping='logical'&gt;"+
" &lt;entity name='savedquery'&gt;"+
" &lt;attribute name='name'/&gt;"+
" &lt;attribute name='savedqueryid'/&gt;"+
" &lt;filter type='and'&gt;"+
" &lt;condition attribute='isprivate' operator='eq' value='false'/&gt;"+
" &lt;condition attribute='inproduction' operator='eq' value='true'/&gt;"+
" &lt;condition attribute='isquickfindquery' operator='eq' value='false'/&gt;"+
" &lt;condition attribute='returnedtypecode' operator='eq' value='"+
document.getElementById('new_entityname').options[document.getElementById('new_entityname').selectedIndex].getAttribute('entitytypeid')+
"'/&gt;"+
" &lt;condition attribute='querytype' operator='eq' value='0'/&gt;"+
" &lt;/filter&gt;"+
" &lt;/entity&gt;"+
"&lt;/fetch&gt;"+
"</fetchXml>"+
"</Fetch>"+
"</soap:Body>"+
"</soap:Envelope>";

var xHReq = new ActiveXObject("Msxml2.XMLHTTP");
xHReq.Open("POST", "/mscrmservices/2007/CrmService.asmx", false);
xHReq.setRequestHeader("SOAPAction","http://schemas.microsoft.com/crm/2007/WebServices/Fetch");
xHReq.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
xHReq.setRequestHeader("Content-Length", xml.length);
xHReq.send(xml);
var resultXml = xHReq.responseXML;
var resultSet = resultXml.text;
resultSet.replace('&lt;','<');
resultSet.replace('&gt;','>');

var oXmlDoc = new ActiveXObject("Microsoft.XMLDOM");
oXmlDoc.async = false;
oXmlDoc.loadXML(resultSet);

//Results Retrieve
var results = oXmlDoc.getElementsByTagName('result');

var ctrl = document.getElementById('new_defaultview');


//Clear picklist values
for(var i = ctrl.options.length-1; i >=0 ; i--)
{
ctrl.options[i] = null;
}

//Fill picklist with retrieved Views
for (var i = 0; i < results.length; i++)
{
var newoption = document.createElement("option");
newoption.setAttribute("value", results[i].selectSingleNode('./savedqueryid').nodeTypedValue);
newoption.innerHTML = results[i].selectSingleNode('./name').nodeTypedValue;
ctrl.appendChild(newoption);
}


Save and Close Form and publish changes. Give all users privileges at least to read this entity.

2. Plugin source:

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.SdkTypeProxy;
using Microsoft.Crm.Sdk.Query;

namespace InternalPVM
{
public class DefaultViewHandler : IPlugin
{
#region IPlugin Members

public void Execute(IPluginExecutionContext context)
{
if (context.PrimaryEntityName == EntityName.savedquery.ToString() &&
context.InputParameters.Contains("Query") &&
context.InputParameters["Query"] is QueryExpression)
{
QueryExpression resultExpression = (QueryExpression)context.InputParameters["Query"];


//I retrieve entity Type code of entity
string entityname = ((ConditionExpression)resultExpression.Criteria.Conditions[0]).Values[0].ToString();

ICrmService crmsevice = context.CreateCrmService(true);

//I create a request to retrieve default View for current entity
QueryExpression query = new QueryExpression("new_defaultuserview");
query.Criteria.AddCondition(new ConditionExpression("new_userid", ConditionOperator.Equal, context.UserId));
query.Criteria.AddCondition(new ConditionExpression("new_entitytypecode", ConditionOperator.Equal, entityname));
query.ColumnSet = new ColumnSet(new string[] { "new_defaultview" });

RetrieveMultipleRequest request = new RetrieveMultipleRequest();
request.ReturnDynamicEntities = true;
request.Query = query;


RetrieveMultipleResponse response = (RetrieveMultipleResponse)crmsevice.Execute(request);

//If there are no settings just exit from this method
if (response.BusinessEntityCollection.BusinessEntities.Count == 0)
return;

//Identifier of Default View
Guid defaultviewid = new Guid(((DynamicEntity)response.BusinessEntityCollection.BusinessEntities[0])["new_defaultview"].ToString());

BusinessEntityCollection result = (BusinessEntityCollection)context.OutputParameters["BusinessEntityCollection"];

//Scroll all Views one-By-One and Set default View from user Setting to be default in resultset
foreach (DynamicEntity de in result.BusinessEntities)
if (((Key)de["savedqueryid"]).Value.Equals(defaultviewid))
de["isdefault"] = new CrmBoolean(true);
else
de["isdefault"] = new CrmBoolean(false);
}
}

#endregion
}
}


Build and register assembly. Register new step - Message RetrieveMultiple, Primary entity - savedquery, Secondary Entity - none, Stage - Post Stage, Execution Mode = Syncronous, Pipeline - Parent Pipeline.

And the demonstration:

23 comments:

  1. Hi a33ik;

    are u sure, you´ve created
    new_defaultview - for default view identifier
    new_defaultview - display name of default View

    (2 times new_defaultview)?

    new_defaultview
    new_displayname is looking much better?

    ReplyDelete
  2. Hi, Carsten.

    I'm very appreciated for your feedback. Of course this is my mistake. Correct attribute name is new_defaultviewdisplayname.

    ReplyDelete
  3. It´s working for almost a week - only thing I was asked: Wouldn´t it be nice to set my personal views as default views as well?

    Well I said - maybe this is something for a next version ;-)

    ReplyDelete
  4. You've deployed my solution on your production?

    ReplyDelete
  5. Yes, tried this in "real life" - and I´m working on some modifications.

    - Entity Picklist sorting a-z
    - Adding personal views as default views
    - Adding support for personal views which are shared
    - Adding support for system views which are shared
    - Adding support to interact with public view manager

    ReplyDelete
  6. the topic is very interesting, but the support of personal views would be great. If somebody has a solution, please post.

    Thanks

    ReplyDelete
  7. This is amazing if I could get past the fact it's telling me i need a default view display name

    ReplyDelete
  8. Set the user defaultview from all views listed in the user view list from "System views" and "My Views" , It much more difficult ,since you need to control 2 retrives and the userquery don't have isdefault attribute.

    ReplyDelete
  9. This looks just like what I need. Can anyone send me the plugin already compiled so all I can to do is register it and create the entity etc.

    ReplyDelete
  10. anyone know if this works in offline mode?

    ReplyDelete
  11. Hi, Withers. I haven't tested it in offline mode. But you just have to register this plugin as offline and test.

    ReplyDelete
  12. Andriy,

    Working on the thread in the Developer forum that references this code. User wants to apply it to the "userquery" instead of "savedquery". I don't know what would be necessary to adjust with your code to achieve that (I'm working on an alternate solution that would convert a "userquery" into a "savedquery"). Any thoughts?

    ReplyDelete
  13. Has any of Carsten's suggestions been implemented into the code. His ideas of
    - Entity Picklist sorting a-z
    - Adding personal views as default views
    - Adding support for personal views which are shared
    - Adding support for system views which are shared
    - Adding support to interact with public view manager

    Looked really useful.

    ReplyDelete
  14. First one can be done. All other - I haven't try but any way it will be unsupported stuff.

    ReplyDelete
  15. dear good job, but i have a error, i change sigla 'new' for 'crm', this is my code

    on change of entitye name
    //Request to retrieve all Public Views for entity
    var xml = ""+
    ""+
    GenerateAuthenticationHeader()+
    ""+
    ""+
    ""+
    "<fetch mapping='logical'>"+
    " <entity name='savedquery'>"+
    " <attribute name='name'/>"+
    " <attribute name='savedqueryid'/>"+
    " <filter type='and'>"+
    " <condition attribute='isprivate' operator='eq' value='false'/>"+
    " <condition attribute='inproduction' operator='eq' value='true'/>"+
    " <condition attribute='isquickfindquery' operator='eq' value='false'/>"+
    " <condition attribute='returnedtypecode' operator='eq' value='"+
    document.getElementById('crm_entityname').options[document.getElementById('crm_entityname').selectedIndex].getAttribute('entitytypeid')+
    "'/>"+
    " <condition attribute='querytype' operator='eq' value='0'/>"+
    " </filter>"+
    " </entity>"+
    "</fetch>"+
    ""+
    ""+
    ""+
    "";

    var xHReq = new ActiveXObject("Msxml2.XMLHTTP");
    xHReq.Open("POST", "/mscrmservices/2007/CrmService.asmx", false);
    xHReq.setRequestHeader("SOAPAction","http://schemas.microsoft.com/crm/2007/WebServices/Fetch");
    xHReq.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
    xHReq.setRequestHeader("Content-Length", xml.length);
    xHReq.send(xml);
    var resultXml = xHReq.responseXML;
    var resultSet = resultXml.text;
    resultSet.replace('<','<');
    resultSet.replace('>','>');

    var oXmlDoc = new ActiveXObject("Microsoft.XMLDOM");
    oXmlDoc.async = false;
    oXmlDoc.loadXML(resultSet);

    //Results Retrieve
    var results = oXmlDoc.getElementsByTagName('result');

    var ctrl = document.getElementById('crm_defaultview');


    //Clear picklist values
    for(var i = ctrl.options.length-1; i >=0 ; i--)
    {
    ctrl.options[i] = null;
    }

    //Fill picklist with retrieved Views
    for (var i = 0; i < results.length; i++)
    {
    var newoption = document.createElement("option");
    newoption.setAttribute("value", results[i].selectSingleNode('./savedqueryid').nodeTypedValue);
    newoption.innerHTML = results[i].selectSingleNode('./name').nodeTypedValue;
    ctrl.appendChild(newoption);
    }
    continue above

    ReplyDelete
  16. plug-in

    using System;
    using System.Collections.Generic;
    using System.Text;
    using Microsoft.Crm.Sdk;
    using Microsoft.Crm.SdkTypeProxy;
    using Microsoft.Crm.Sdk.Query;

    namespace InternalPVM
    {
    public class DefaultViewHandler : IPlugin
    {
    #region IPlugin Members

    public void Execute(IPluginExecutionContext context)
    {

    if (context.PrimaryEntityName == EntityName.savedquery.ToString() &&
    context.InputParameters.Contains("Query") &&
    context.InputParameters["Query"] is QueryExpression)
    {
    QueryExpression resultExpression = (QueryExpression)context.InputParameters["Query"];


    //I retrieve entity Type code of entity
    string entityname = ((ConditionExpression)resultExpression.Criteria.Conditions[0]).Values[0].ToString();

    ICrmService crmsevice = context.CreateCrmService(true);

    //I create a request to retrieve default View for current entity
    QueryExpression query = new QueryExpression("crm_defaultuserview");
    query.Criteria.AddCondition(new ConditionExpression("crm_userid", ConditionOperator.Equal, context.UserId));
    query.Criteria.AddCondition(new ConditionExpression("crm_entitytypecode", ConditionOperator.Equal, entityname));
    query.ColumnSet = new ColumnSet(new string[] { "crm_defaultview" });

    RetrieveMultipleRequest request = new RetrieveMultipleRequest();
    request.ReturnDynamicEntities = true;
    request.Query = query;


    RetrieveMultipleResponse response = (RetrieveMultipleResponse)crmsevice.Execute(request);

    //If there are no settings just exit from this method
    if (response.BusinessEntityCollection.BusinessEntities.Count == 0)
    return;

    //Identifier of Default View
    Guid defaultviewid = new Guid(((DynamicEntity)response.BusinessEntityCollection.BusinessEntities[0])["crm_defaultview"].ToString());

    BusinessEntityCollection result = (BusinessEntityCollection)context.OutputParameters["BusinessEntityCollection"];

    //Scroll all Views one-By-One and Set default View from user Setting to be default in resultset
    foreach (DynamicEntity de in result.BusinessEntities)
    if (((Key)de["savedqueryid"]).Value.Equals(defaultviewid))
    de["isdefault"] = new CrmBoolean(true);
    else
    de["isdefault"] = new CrmBoolean(false);
    }
    }

    #endregion
    }
    }


    but its show me a errors, tipycal microsoft .. try again blabla...

    please help me!!

    thank for all

    ReplyDelete
  17. dear my problem is when press save button form, you can help me please!

    thanks

    ReplyDelete
  18. First off Andriy - great work - this has got me 90% of the way. I wonder if anyone has been able to implement a version that sets the default view to a user query view. Its easy enough to incorporate user query views into the Default View entity, the problem arises when you try and set the view to default in the plugin (as has already been pointed out there is no 'isdefault' equivalent in the userquery model).

    ReplyDelete
  19. Hello Mark,

    I'm afraid that it is impossible with supported methods. You can try to use unsupported approach - editing of aspx file to perform your task.

    ReplyDelete
  20. I thought as much, though :
    1. I'm not sure I want to edit core ASPX files
    2. I don't know which ASPX to edit

    I've got a clunky way round it that involves creating 'client specific' public views and setting them as the default view on the various entities we allow access to. Its a pain in that we need to publish each time a new client wants access (which fortunately isn't very often).

    My next step is to allow IFD clients to search there default views.

    ReplyDelete
  21. This comment has been removed by the author.

    ReplyDelete
  22. Hi and thanks for your code.
    I made a function to order the entities alphabetically, but this editor truncates the code for it, so it will be useless to put it here.

    ReplyDelete
  23. Thanks,
    After several implementations of this code I have implemented the same thing but I decided not to publish it here.

    ReplyDelete