Friday, May 15, 2009

Public Views Manager For Microsoft Dynamics CRM 4.0 With Own Hands

Following article describes how to create mechanism which provides/restricts access to views of entities.

Idea:
1. Creation two custom entities - Public View Manager(contains name of entity to restrict access to views and Business Unit for which restriction will be operate) and Public View Detail (contains View Name and Bit flag - Access Allowed/Disallowed).
Public View Manager entity is parent to Public View Detail. Both entities are organization owned.
2. Public Entity Manager form scripting.
3. Writing a plugin which will create Public View Detail for just created Public View Manager.
4. Writing a plugin which will handle RetrieveMultiple message on savedquery entity and if restrictions exist - correct filter for data retrieving.


1.1. Creation of Public View Manager 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 required attributes:
new_bu - Display name of Business Unit
new_buid - identifier of Business Unit
new_entitydisplayname - display name of entity (entity schema name will be stored in new_entityname field)
new_entitytypecode - entity type code :)

I design the appearance of form:



Iframe I have added for child details display.
Second tab will be hidden. Here is it:



1.2. Child entity creation:



Here I input Display Name, Plural Name, ownership is organization, name of entity.



I set primary attribute name as new_viewname.





I've added the relationship between Public View Manager and Public View Detail.



I create required attributes:
new_isallowed - attribute show accessibility of view
new_publicviewmanagerid - lookup attribute - to the primaru entity Public View Manager
new_viewid - identifier of view (savedquery)



I configure form appearance.

2. Public View Manager OnLoad script:

//Method converts EntityName field to picklist with entities list
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'>";
var defaultValueFound = false;

for (var i = 0; i < dataItems.length; i++)
//filter on only Customizable entities
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;
}

//Method converts Business Unit field to picklist with Business Unit List
function ConvertBUToPicklist(fieldName, dataItems)
{
var defaultValue = crmForm.all.new_buid.DataValue;

if (defaultValue == null)
{
defaultValue = dataItems[0].selectSingleNode('./q1:businessunitid').nodeTypedValue;
crmForm.all.new_buid.DataValue = defaultValue
}


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('./q1:businessunitid').nodeTypedValue + "'";
if (dataItems[i].selectSingleNode('./q1:businessunitid').nodeTypedValue == defaultValue)
{
select += " SELECTED";
defaultValueFound = true;
}
select += ">" + dataItems[i].selectSingleNode('./q1:name').nodeTypedValue + "</option>";
}

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

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

//Hiding tab with hidden controls
crmForm.all.tab1Tab.style.display = 'none';

//Forming the request to the metadata service to retrieve full list of 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);

//send the request to the metadata service

xmlHttpRequest.send(request);

//and retrieve result

var result = xmlHttpRequest.responseXML;
var schemaNames = result.selectNodes("//CrmMetadata/CrmMetadata");

//call convert method
ConvertEntityToPicklist('new_entityname', schemaNames);


//create a request to Crm Service to retrieve list of business units

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>"+
"<RetrieveMultiple xmlns='http://schemas.microsoft.com/crm/2007/WebServices'>"+
"<query xmlns:q1='http://schemas.microsoft.com/crm/2006/Query'"+
" xsi:type='q1:QueryExpression'>"+
"<q1:EntityName>businessunit</q1:EntityName>"+
"<q1:ColumnSet xsi:type='q1:ColumnSet'>"+
"<q1:Attributes>"+
"<q1:Attribute>name</q1:Attribute>"+
"</q1:Attributes>"+
"</q1:ColumnSet>"+
"<q1:Distinct>false</q1:Distinct>"+
"</query>"+
"</RetrieveMultiple>"+
"</soap:Body>"+
"</soap:Envelope>";

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

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

//send the request
xmlHttpRequest.send(request);

//retrieve data
result = xmlHttpRequest.responseXML;

var BUs = result.getElementsByTagName('BusinessEntity');

//call convert method
ConvertBUToPicklist('new_buid', BUs);

if (crmForm.FormType != 1)
{
//if opened form - is form of already created records - I disable controls
document.getElementById('new_entityname_d').disabled = true;
document.getElementById('new_buid_d').disabled = true;

//form url for IFrame which show child entities
var url = "areas.aspx?";
url += "oId=" + crmFormSubmit.crmFormSubmitId.value;
url += "&oType=" + crmFormSubmit.crmFormSubmitObjectType.value;
url += "&security=" + crmFormSubmit.crmFormSubmitSecurity.value;
url += "&tabSet=new_manager_publicviewdetail";
crmForm.all.IFRAME_Views.src = url;


//hide menubar
crmForm.all.IFRAME_Views.attachEvent("onreadystatechange", Ready);

function Ready()
{
var doc = crmForm.all.IFRAME_Views.contentWindow.document;
if (doc.getElementById("mnuBar1") != null)
doc.getElementById("mnuBar1").style.display = "none";
}

}


Public View Manager OnSave script:

if (crmForm.FormType == 1)
{
crmForm.all.new_bu.DataValue = document.getElementById('new_buid').options[document.getElementById('new_buid').selectedIndex].innerText;

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

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


3. Plugin for creation Child entities for public view manager. Step must be registered as a Post Syncronous Create to new_publicviewmanager entity. Code of the plugin:

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

namespace PublicViewManager
{
public class PublicViewManagerCreate : IPlugin
{

#region CTOR

public PublicViewManagerCreate(string config, string secureConfig)
{
}

#endregion CTOR

#region IPlugin Members

public void Execute(IPluginExecutionContext context)
{
try
{
if (context.MessageName == MessageName.Create &&
context.InputParameters.Contains("Target") &&
context.InputParameters["Target"] is DynamicEntity &&
((DynamicEntity)context.InputParameters["Target"]).Name == "new_publicviewmanager")
{
DynamicEntity pvm = (DynamicEntity)context.InputParameters["Target"];

//name of entity
string entityname = (string)pvm["new_entityname"];


//id of public view manager
Guid pvmId = (Guid)context.OutputParameters["Id"];

CrmService crmService = GetCrmService(context.OrganizationName);

//creation of query to retrieve the list of related to entity public views

ConditionExpression condition1 = new ConditionExpression("isprivate", ConditionOperator.Equal, false);
ConditionExpression condition2 = new ConditionExpression("inproduction", ConditionOperator.Equal, true);
ConditionExpression condition3 = new ConditionExpression("isquickfindquery", ConditionOperator.Equal, false);
ConditionExpression condition4 = new ConditionExpression("returnedtypecode", ConditionOperator.Equal, entityname.ToLower());
ConditionExpression condition5 = new ConditionExpression("querytype", ConditionOperator.Equal, 0);

FilterExpression filter = new FilterExpression();
filter.Conditions.AddRange(new ConditionExpression[] { condition1, condition2, condition3, condition4, condition5 });

QueryExpression query = new QueryExpression(EntityName.savedquery.ToString());
query.ColumnSet = new AllColumns();

query.Criteria = filter;

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

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

//response reading and cyclic creation of detail records
foreach (DynamicEntity sq in response.BusinessEntityCollection.BusinessEntities)
{
DynamicEntity detail = new DynamicEntity("new_publicviewdetail");
detail["new_viewname"] = (string)sq["name"];
detail["new_viewid"] = ((Key)sq["savedqueryid"]).Value.ToString();
detail["new_publicviewmanagerid"] = new Lookup("new_publicviewmanager", pvmId);
detail["new_isallowed"] = new CrmBoolean(true);

crmService.Create(detail);
}
}
}
catch (System.Web.Services.Protocols.SoapException exc)
{
throw new Exception(exc.Detail.InnerXml);
}
}

#endregion

protected CrmService GetCrmService(string OrgName)
{
CrmAuthenticationToken token = new CrmAuthenticationToken();
token.AuthenticationType = AuthenticationType.AD;
token.OrganizationName = OrgName.Replace(" ", "");

CrmService crmService = new CrmService();
crmService.UseDefaultCredentials = true;
crmService.Url = (string)(Registry.LocalMachine.OpenSubKey("Software\\Microsoft\\MSCRM").GetValue("ServerUrl")) + "/2007/crmservice.asmx";
crmService.CrmAuthenticationTokenValue = token;

return crmService;
}

}
}


4. Plugin on RetrieveMultiple entity which will insert additional filters to query. Step must be registered as a Pre Syncronous RetrieveMultiple on savedquery entity.

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

namespace PublicViewManager
{
public class SavedQueryRetrieveHandler : IPlugin
{

#region CTOR

public SavedQueryRetrieveHandler(string config, string secureConfig)
{
}

#endregion CTOR


#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"];


//entity type code retrieving
string entityTypeCode = ((ConditionExpression)resultExpression.Criteria.Conditions[0]).Values[0].ToString();

ICrmService crmService = context.CreateCrmService(true);

//reading current user business unit
systemuser user = (systemuser)crmService.Retrieve(EntityName.systemuser.ToString(), context.UserId, new ColumnSet(new string[] { "businessunitid" }));
Guid buid = user.businessunitid.Value;

//creation of query to retrieve existing restrictions
QueryExpression expression = new QueryExpression("new_publicviewdetail");
expression.ColumnSet = new ColumnSet(new string[] { "new_viewid" });
expression.Criteria.AddCondition("new_isallowed", ConditionOperator.Equal, false);

LinkEntity link = expression.AddLink("new_publicviewmanager", "new_publicviewmanagerid", "new_publicviewmanagerid");
link.LinkCriteria.AddCondition("new_entitytypecode", ConditionOperator.Equal, entityTypeCode);
link.LinkCriteria.AddCondition("new_buid", ConditionOperator.Equal, "{" + buid.ToString().ToUpper() + "}");

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

//restrictions retrieving
List<BusinessEntity> restictions = ((RetrieveMultipleResponse)crmService.Execute(request)).BusinessEntityCollection.BusinessEntities;

//inserting additional filters to query to filter out all restricted views
foreach (DynamicEntity restriction in restictions)
resultExpression.Criteria.AddCondition("savedqueryid", ConditionOperator.NotEqual, restriction["new_viewid"].ToString());

context.InputParameters["Query"] = resultExpression;
}
}

#endregion IPlugin Members
}
}


And the result. I hope I will put the video here soon. Now screenshots:

Contact views before adding filters:



Creation of a new Public View Manager record to contact entity:



Form of Public View Manager with created details for all views:



New filters to views are added:



And result - three marked views are absent in the views dropdown:



And video. I am sorry for bad video quality.

20 comments:

  1. Having trouble with your second plugin. After registering step, I can´t open any entity. Unregister the step, I can access all of my entities. Maybe this error is because of multiple tenants?

    I can´t find any error in the code. The only change, I´ve done so far is instead of new_ my prefix is cgr_, but I´ve changed this in every codeline.

    ReplyDelete
  2. Hi, Carsten.

    I'm very appreciated for your response.

    Have you strongnamed your assembly?
    Did you try to debug assembly?

    I develop all plugins on server with few organization, so the source of error npt in multitanency.

    ReplyDelete
  3. Hi,

    all assemblies strongnamed - no luck. Looks the error is harder to debug (maybe another assembly which would not work with yours). It would be nicer to have all source files (customization.xml and .dlls). Maybe you´ll post them here, so I can load them in my dev environment.

    ReplyDelete
  4. Here they are.

    http://rapidshare.com/files/235902275/PublicViewManager.zip.html

    ReplyDelete
  5. And another one question - now do you register your steps?

    ReplyDelete
  6. Of course I did ;-). I´m developing crm solutions too, so I´m not a newbee ;-)

    ReplyDelete
  7. Oh. Sorry. It was the misprint... Not Now, but How - how do you register your steps (Stage, Execution Mode, Pipeline).

    ReplyDelete
  8. With your source files it is working now. The only difference I could see was by blogpost-design. I thought there were two plugin-files (.dlls). Now I´ve one .dll with two classes inside. Therefore registering my second plugin caused the errors while retrieving savedqueries.

    Great solution. I´ll translate that for the german users and will post this on my blog, if you don´t mind...

    ReplyDelete
  9. Thanks. I will be appreciated if you in your post will post the URL to this topic in my blog =)

    ReplyDelete
  10. article online: http://carstengroth.spaces.live.com/blog/cns!97768EC3728C1FF3!669.entry

    thanks a lot

    ReplyDelete
  11. Thank you, Carsten.

    I hope my future posts will be more interesting than current =)

    ReplyDelete
  12. Me again: In a production environment, I get some strange behaviour - Users can´t view entities, because of missing user rights (error message). Unregistering the step "savedquery" will solve the problem, but this would cause a non working public views manager. Which user rights the user must have to get both worlds working. Did you figured this out?

    ReplyDelete
  13. Hi. Set in user roles rights for read to PublicViewManager and publicviewmanagerdetail entities. This will solve your problems.

    ReplyDelete
  14. Of course! This was my fault. Thanks

    ReplyDelete
  15. Does everything work fine now?

    ReplyDelete
  16. For idea - you can also add filters by security roles and users. This will made functionality more flexible.

    ReplyDelete
  17. Hi,

    I have the same problem like Carsten. Everthing works but when I register the savedqueryretrieve no entity is functionable in crm. When I deregister this everything is ok.
    I try this but with no effect...:
    Set in user roles rights for read to PublicViewManager and publicviewmanagerdetail entities. This will solve your problems..

    Andreas

    ReplyDelete
  18. Hi, Andreas.

    This is very strange. If this works for others this have to work for you. Could you place somewhere screen-shot of role you use for users and place here the url?

    ReplyDelete
  19. Hi
    I have a problem with this solution.
    when I login as a salesperson MSCRM alert me like this :

    " Insufficient Permissions
    The logged-on user does not have the appropriate security permissions to view these records or perform the specific action.

    Try this action again. If the problem continues, check the Microsoft Dynamics CRM Community for solutions or contact your organization's Microsoft Dynamics CRM Administrator. Finally, you can contact Microsoft Support. "

    I couldn't find any solution for that...
    please help

    ReplyDelete
  20. Hi Andriy, it's possible send me your plugin? I don't know what I'm doing wrong but when I registry plugin, CRM shutdown. Thank for your help
    Radek
    repisa@seznam.cz

    ReplyDelete