Programming Languages Hacks

Importanti regole per linguaggi di programmazione rilevanti come Java, C, C++, C#…

  • Subscribe

  • Lettori

    I miei lettori abituali

  • Twitter

Customizzare Microsoft Dynamics CRM 4.0

Posted by Ricibald on February 5th, 2009

Microsoft Dynamics CRM 4.0 nasce come pacchetto immediatamente utilizzabile, ma in realtà deve essere personalizzato per mostrare le cose che effettivamente servono all’utente finale e rinominando concetti che potrebbero non esistere nel dominio applicativo.

Prendiamo il caso di una pubblica amministrazione che voglia avere un CRM. In questo caso il concetto di “cliente” deve essere rinominato in “cittadino”. Alcune proprietà del “cittadino” non avranno più senso di esistere, mentre saranno necessarie altre proprietà completamente nuove.

Microsoft Dynamics CRM 4.0 consente di:

  • aggiungere nuove entità (una nuova struttura dati, come “diritto civile”)
  • aggiungere nuovi attributi (come la proprietà “anni galera” su un entità Contact)
  • cambiare il nome di un’entità (“contact” diventa “citizen”)
  • aggiungere o cambiare viste
  • aggiungere o cambiare form
  • creare relazioni (“diritto civile” ha relazione N:N con “contact”)
  • importare ed esportare le customizzazioni

Dovrebbe essere sempre buona regola verificare attentamente che gli attributi/entità che stiamo aggiungendo non siano già presenti sotto altro nome.
La creazione di un nuovo attributo consente di specificare il tipo del valore:

  • nvarchar: semplice testo fino a 4000 caratteri. Sono personalizzabili modalità di visualizzazione e comportamento del testo
  • ntext: casella di testo fino a 100.000 caratteri
  • int: semplice intero. Si può specificare il format vincolando l’intero ad assumere solo un certo range di valori
  • float: semplice float. Si può impostare la precisione, valori minimi e massimi.
  • money: semplice decimal. Supporta transazioni in più valute, grazie alla memorizzazione del valore in un formato wrappato indipendente dalla valuta (base currency), chiamato base attribute
  • bit: semplice booleano. Si può associare nel caso affermativo/negativo. Nella form si può specificare se mostrarlo come checkbox, 2 radio o dropdown
  • datetime: data, mostrata coin widget calendario. Si può scegliere se mostrare data e ora oppure solo data
  • picklist: dropdown che consente di memorizzare un enumerato, di cui si può specificare l’intero corrispondente
  • lookup: vincoli verso altre entità. Sono attributi creabili tramite relazioni N:1 con un’altra entità
  • partylist: molteplici lookup. Proprietà che rappresentano relazioni N:N ma che non è possibile creare

Ogni attributo può avere associati i seguenti field constraint:

  • Business Required: obbligatorio per il salvataggio. Mostrato con * rosso
  • Business Recommended: consigliato ma non obbligatorio. Mostrato con + blu
  • No Constraint (predefinito): campo normale

Ogni relazione può avere definito il tipo di comportamento a catena (cascade behaviour) nel caso di eventi avvenuti nell’entità collegata (come l’eliminazione). Durante la definizione di una relazione si può specificare il behaviour type:

  • Parental: ogni azione viene anche applicata sulle entità child collegate. Ogni entità può avere al massimo una relazione parental. Infatti, anche se si possono creare tante relazioni N:1 quante se ne vuole, solo una può essere Parental, e tutte le altre Referential.
  • Referential: le azioni nel record padre non impattano nel record figlio
  • Referential, Restrict Delete: le azioni nel record padre non impattano nel record figlio, ma il record padre non può essere eliminato finché esiste il record figlio
  • Configurable Cascading: ogni azione può essere opzionalmente anche applicata sulle entità child. Ma, a prescindere, se si elimina un’entità padre, ogni record dell’entità figlio sono automaticamente eliminate

La Navigation Pane sulla sinistra può essere customizzata e profilata utilizzando il Site Map o il link Personalize Workspace o il link Options dentro Tools. Per customizzare tale file è necessario prima esportarlo e poi reimportarlo una volta modificato.

Esempio di una porzione del sitemap [reference]:

<SiteMap>
  <Area Id="Workplace" ResourceId="Area_Workplace" ShowGroups="true" Icon="/_imgs/workplace_24x24.gif" DescriptionResourceId="Workplace_Description">
    <Group Id="MyWork" ResourceId="Group_MyWork" DescriptionResourceId="My_Work_Description">
      <SubArea Id="nav_activities" Entity="activitypointer" DescriptionResourceId="Activities_SubArea_Description" Url="/Workplace/home_activities.aspx" />
      <SubArea Id="nav_calendar" Icon="/_imgs/area/18_calendar.gif" ResourceId="Homepage_Calendar" Url="/workplace/home_calendar.aspx" Client="Web">
        <Privilege Entity="activitypointer" Privilege="Read" />
      </SubArea>
      <SubArea Id="nav_import" Icon="/_imgs/area/18_import.gif" ResourceId="Homepage_Import" Url="/workplace/home_import.aspx" DescriptionResourceId="Imports_Description">
        <Privilege Entity="import" Privilege="Read" />
      </SubArea>
      <SubArea Id="nav_duplicatedetectionjobs" Icon="/_imgs/data_management.gif" ResourceId="Homepage_DuplicateDetectionJobs" Url="/Tools/DuplicateDetection/SystemWideDuplicateDetection/home_duplicatedetectionjobs.aspx" DescriptionResourceId="DuplicateDetectionJobs_Description">
        <Privilege Entity="asyncoperation" Privilege="Read" />
      </SubArea>
      <SubArea Id="nav_queues" Entity="queue" Url="/workplace/home_workplace.aspx" DescriptionResourceId="Queues_SubArea_Description">
        <Privilege Entity="activitypointer" Privilege="Read" />
      </SubArea>
      <SubArea Id="nav_answers" Entity="kbarticle" Url="/workplace/home_answers.aspx" DescriptionResourceId="Article_SubArea_Description">
        <Privilege Entity="subject" Privilege="Read" />
      </SubArea>
      <SubArea Id="nav_reports" Entity="report" Url="/CRMReports/home_reports.aspx" DescriptionResourceId="Reports_Description">
        <Privilege Entity="report" Privilege="Read" />
      </SubArea>
      <SubArea Id="nav_news" Entity="businessunitnewsarticle" Url="/home/homepage/home_news.aspx" DescriptionResourceId="News_SubArea_Description" />
    </Group>
    <Group Id="Customers" ResourceId="Group_Customers" DescriptionResourceId="Customers_Description">
      <SubArea Id="nav_accts" Entity="account" DescriptionResourceId="Account_SubArea_Description" />
      <SubArea Id="nav_conts" Entity="contact" DescriptionResourceId="Contact_SubArea_Description" />
    </Group>
    <Group Id="SFA" ResourceId="Area_Sales" IsProfile="true" DescriptionResourceId="Sales_Description">
      <SubArea Id="nav_lists1" Entity="list" DescriptionResourceId="MarketingList_SubArea_Description" />
      <SubArea Id="nav_leads" Entity="lead" DescriptionResourceId="Lead_SubArea_Description" />
      <SubArea Id="nav_oppts" Entity="opportunity" DescriptionResourceId="Opportunity_SubArea_Description" />
      <SubArea Id="nav_quotes" Entity="quote" DescriptionResourceId="Quote_SubArea_Description" />
      <SubArea Id="nav_orders" Entity="salesorder" DescriptionResourceId="Orders_SubArea_Description" />
      <SubArea Id="nav_invoices" Entity="invoice" DescriptionResourceId="Invoice_SubArea_Description" />
    </Group>
    <Group Id="MA" ResourceId="Area_Marketing" IsProfile="true" DescriptionResourceId="Marketing_Description">
      <SubArea Id="nav_lists" Entity="list" DescriptionResourceId="MarketingList_SubArea_Description" />
      <SubArea Id="nav_campaigns" Entity="campaign" DescriptionResourceId="Campaign_SubArea_Description" Url="/MA/home_camps.aspx" />
      <SubArea Id="nav_minicamps" Entity="bulkoperation" DescriptionResourceId="Quick_Campaign_Description" Icon="/_imgs/ico_18_minicamps.gif" OutlookShortcutIcon="/_imgs/olk_4400.ico" Url="/MA/home_minicamps.aspx">
        <Privilege Privilege="AllowQuickCampaign" />
      </SubArea>
    </Group>
    <Group Id="CS" ResourceId="Area_Service" IsProfile="true" DescriptionResourceId="Customer_Service_Description">
      <SubArea Id="nav_cases" Entity="incident" DescriptionResourceId="Cases_SubArea_Description" Url="/CS/home_cases.aspx" />
      <SubArea Id="nav_managekb" ResourceId="Homepage_KBManager" DescriptionResourceId="KBManager_SubArea_Description" Icon="/_imgs/ico_18_126.gif" Url="/cs/home_managekb.aspx" AvailableOffline="false">
        <Privilege Entity="kbarticle" Privilege="Read,Write,Create" />
      </SubArea>
      <SubArea Id="nav_contracts" Entity="contract" DescriptionResourceId="Contract_SubArea_Description" />
    </Group>
    <Group Id="SM" ResourceId="Area_Scheduling" IsProfile="true" DescriptionResourceId="Scheduling_Group_Description">
      <SubArea Id="nav_apptbook" ResourceId="Homepage_AppointmentBook" DescriptionResourceId="AppointmentBook_SubArea_Description" Icon="/_imgs/ico_18_servicecal.gif" Url="/sm/home_apptbook.aspx" AvailableOffline="false">
        <Privilege Entity="activitypointer" Privilege="Read" />
        <Privilege Entity="service" Privilege="Read" />
      </SubArea>
    </Group>
  </Area>
</SiteMap>

La struttura riflette la seguente immagine:

Alcune note:

  • Area: rappresenta il “pulsantone sulla sinistra”, in questo caso il Workplace
  • Group: consente di organizzare i pulsanti di un area, in modo simile agli accordion panel. Se vale IsProfile, allora il gruppo può essere mostrato o meno a seconda della scelta personale dell’utente (tramite “Personalize Workspace…”)
  • SubArea: rappresenta il pulsante di un Area (Activities, Calendar, …). Può essere assegnato in alternativa un URL o un Entity. Quest’ultimo contiene l’identificatore dell’entity da mostrare (di cui verrà visualizzata la “pubic default view”
  • Privilege: visualizza la SubArea solo agli utenti che hanno determinati privilegi nell’Entity specificata

Per creare pulsanti custom, menu e aree di navigazione per form e view si utilizza ISV.Config (ISV = Independent Service Vendor). Ogni item può aprire uno specifico URL o creare del codice jscript che utilizza funzioni jscript documentate nell’sdk. Per modificare l’ISV.Config si segue la stessa procedura del site map.

Esempio di una porzione del ISV.Config [reference]:

<IsvConfig>
  <configuration version="3.0.0000.0">
    <Root>
      <Entities>
        <Entity name="account">
          <!-- The Account Tool Bar -->
          <ToolBar ValidForCreate="0" ValidForUpdate="1">
            <Button Icon="/_imgs/logo_16.gif" PassParams="1" WinMode="0" ValidForCreate="0" ValidForUpdate="1" JavaScript="var ObId=document.getElementById('crmFormSubmitId').value; var ObTp=document.getElementById('crmFormSubmitObjectTypeName').value; window.open('page.aspx?id=' + ObId + '&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;type=' + ObTp);">
              <Titles>
                <Title LCID="1033" Text="Provide" />
              </Titles>
              <ToolTips>
                <ToolTip LCID="1033" Text="Provide" />
              </ToolTips>
            </Button>
          </ToolBar>
        </Entity>
      </Entities>
    </configuration>
</IsvConfig>

L’ISV.Config riportato aggiunge un nuovo pulsante “Provide” all’entità account nella barra degli strumenti, mostrato quando si modifica un account esistente.

Alcune note:

  • la Toolbar rappresenta la barra degli strumenti nella form di dettaglio
  • sia il toolbar che il button possono essere validi per la form di creazione (ValidForCreate) e/o per la form di aggiornamento (ValidForUpdate)
  • PassParams consente di passare dei parametri GET all’url invocato in modo tale che sia possibile ricostruire il contesto del chiamante (entity di lavoro, id dell’organizzazione, …)
  • WinMode consente di configurare il comportamento della finestra (window, modal, modeless)
  • Javascript o Url: uno dei due deve essere specificato e rappresenta il comportamento alla pressione
  • elementId utilizzati nel javascript: “crmFormSubmitId” è l’id dell’entità in editing; “crmFormSubmitObjectTypeName” è il tipo di entità che si stà lavorando (“account”)

Altro ISV.Config di esempio:

<IsvConfig>
  <configuration version="3.0.0000.0">
    <Root>
      <Entities>
        <Entity name="contact">
          <Grid>
            <MenuBar>
              <Buttons>
                <Button Icon="/ISV/VE/img/LiveLocal3D.ico" PassParams="1" WinMode="0" JavaScript="var selected_contacts = getSelected('crmGrid'); window.open('page.aspx?sel=' + selected_contacts);">
                  <Titles>
                    <Title LCID="1033" Text="Visualizza" />
                  </Titles>
                  <ToolTips>
                    <ToolTip LCID="1033" Text="Visualizza" />
                  </ToolTips>
                </Button>
              </Buttons>
            </MenuBar>
          </Grid>
        </Entity>
      </Entities>
    </configuration>
</IsvConfig>

Questo ISV.Config consente di effettuare lavoro sulla selezione di molteplici elementi sulla griglia, utilizzando la funzione javascript getSelected.

Infine, se si vuole interrogare un web service si può utilizzare la funzione javascript RemoteCommand, che consente di inviare un comando remoto (localizzato in Inetpub\wwwroot\_controls\RemoteCommands\RemoteCommand.js). Ad esempio:

var ObId=document.getElementById('crmFormSubmitId').value;
try {    
    var oProgressRetrieveCommand = new RemoteCommand('WebServiceName', 'Method', 'Path');
    oProgressRetrieveCommand.ErrorHandler = emptyErrorHandler;
    oProgressRetrieveCommand.SetParameter('objectId', ObId);
    var oResult = oProgressRetrieveCommand.Execute();

    if (oResult.Success)
    {
        var Return = oResult.ReturnValue;
        alert(Return.Success+', '+Return.Value);
    } else {
        alert('Fail')
    }
} catch(e) {
   alert('Error: '+e.message);
}

Molto spesso i javascript customizzati per un certo pulsante devono essere raffinati e testati. Risulta quindi molto scomodo configurare il javascript direttamente dentro il file ISV.config. Per questo conviene all’interno dell’ISV.config richiamare una funzione javascript importata. Risulta quindi necessario dichiarare un nuovo file js da importare nell’header della pagina. Questo non risulta possibile in Microsoft CRM, ma si può procedere andando a gestire l’evento OnLoad della nostra form di interesse andando ad inserire il codice seguente:

var script = document.createElement("script");
script.type = "text/javascript";
script.src= '../../js/myscript.js'
document.getElementsByTagName("head")[0].appendChild(script);

Per aggiungere funzionalità completamente custom si utilizzano gli iframe. In generale non si dovrebbe utilizzare nulla che non sia documentato nel Microsoft Dynamics CRM SDK, poiché si potrebbero avere funzioni “non ufficiali” che cambiano tra una versione e l’altra del crm.

Una piccola nota: l’ISV.Config per funzionare deve essere abilitato. Per questo si devono selezionare tutti gli elementi da Settings, Administration, System Settings, Customization, Custom menus and toolbars.

Un esempio di come utilizzare le API per ottenere le nostre custom properties:


[WebMethod]
public ArrayList Get_Account_Info(string idCompany, string nameCompany)
{
	ArrayList risultato=new ArrayList();
	Guid companyID = new Guid(idCompany);
	CrmServiceWsdl.CrmService service = GetCrmService(nameCompany);
	CrmServiceWsdl.TargetRetrieveDynamic target = new CrmServiceWsdl.TargetRetrieveDynamic();
	target.EntityId = companyID;
	target.EntityName = CrmServiceWsdl.EntityName.account.ToString();
	CrmServiceWsdl.ColumnSet columnSet = new CrmServiceWsdl.ColumnSet();

	// Aggiungere altre proprieta' se necessario
	columnSet.Attributes = new string[] { "name","new_friendlyname","new_ncasellediposta","new_nutenti","new_stato" };
	// Create a retrieve request object.
	CrmServiceWsdl.RetrieveRequest retrieve = new CrmServiceWsdl.RetrieveRequest();
	retrieve.Target = target;
	retrieve.ColumnSet = columnSet;
	retrieve.ReturnDynamicEntities = true;
	// Create a response reference and execute the retrieve request.
	CrmServiceWsdl.RetrieveResponse response;
	CrmServiceWsdl.DynamicEntity retrievedEntity;
	try
	{
		response = (CrmServiceWsdl.RetrieveResponse)service.Execute(retrieve);
		retrievedEntity = (CrmServiceWsdl.DynamicEntity)response.BusinessEntity;
	}
	catch (SoapException ex)
	{
		return new EntityProps[] { new EntityProps("error", ex.Detail.InnerXml) };
	}
	// Access the retrieved properties of the dynamic entity.
	foreach (CrmServiceWsdl.Property prop in retrievedEntity.Properties)
	{
		if (prop is CrmServiceWsdl.PicklistProperty)
		{
			//Un intero rappresentante lo stato
			risultato.Add( new EntityProps( prop.Name,((CrmServiceWsdl.PicklistProperty)prop).Value.Value));
		}
		if(prop is CrmServiceWsdl.CrmNumberProperty)
		{
			//Un valore numerico (int!)
			risultato.Add( new EntityProps(prop.Name,((CrmServiceWsdl.CrmNumberProperty)prop).Value.Value));
		}
		if(prop is CrmServiceWsdl.StringProperty)
		{
			//Una stringa (come ad es nome univoco)
			risultato.Add( new EntityProps(prop.Name, ((CrmServiceWsdl.StringProperty)prop).Value));
		}
	}
	if(risultato.Count==0)
	return new EntityProps[] { new EntityProps("error", "Error: cannot find Company")};
	return res;
}

private CrmServiceWsdl.CrmService GetCrmService(string organizationName)
{
	// Get the CRM Users appointments
	// Setup the Authentication Token
	CrmServiceWsdl.CrmAuthenticationToken token = new CrmServiceWsdl.CrmAuthenticationToken();
	token.OrganizationName = organizationName;
	CrmServiceWsdl.CrmService service = new CrmServiceWsdl.CrmService();
	if (CRMServer != null &amp;amp;amp;amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp;amp;amp;amp; CRMServer.Length > 0)
	{
		service.Url = "http://" + CRMServer + "/MSCRMServices/2007/CrmService.asmx";
	}
	service.Credentials = new NetworkCredential("administrator", "password"); //System.Net.CredentialCache.DefaultCredentials;
	service.CrmAuthenticationTokenValue = token;
	return service;
}

Senza entrare nel dettaglio di ogni singola funzione, si richiede al web service CRM la Execute di una Request (RetrieveRequest), che restituisce una Response (RetrieveResponse). A seconda del tipo di richiesta avremo una UpdateRequest e una corrispondente UpdateResponse e così via. La RetrieveRequest contiene i parametri della query (TargetRetrieveDynamic) e cosa si aspetta in output (ColumnSet). La RetrieveResponse restituisce una BusinessEntity (DynamicEntity) che contiene le varie proprietà da ispezionare che derivano tutte dalla classe base Property e la cui implementazione varia a seconda del tipo.

In realtà questo approccio, anche se generico, non è quello ottimo. Infatti, è possibile richiedere al CRM il WSDL aggiornato, che comprenderà anche l’accesso alle nostre nuove proprietà. Questo consente di richiamare le proprietà customizzate in maniera diretta e fortemente tipizzata attraverso l’esecuzione della Retrieve (esiste in modo analogo il metodo Update). Ad esempio:

[WebMethod]
public ArrayList Get_Account_Info(string idAccount, string nameAccount)
{
	ArrayList risultato=new ArrayList();
	Guid accountID = new Guid(idAccount);
	CrmServiceWsdl.CrmService service = GetCrmService(nameAccount);
	CrmServiceWsdl.EntityName entityName = CrmServiceWsdl.EntityName.account.ToString();
	CrmServiceWsdl.ColumnSet columnSet = new CrmServiceWsdl.ColumnSet();
	columnSet.Attributes = new string[] { "name","new_friendlyname","new_ncasellediposta","new_nutenti","new_stato" };
	
	// Ottieni account
	account accountRetrieved;
	try
	{
		accountRetrieved = (CrmServiceWsdl.account)service.Retrieve(entityName, accountID, columnSet);
	}
	catch (SoapException ex)
	{
		return new EntityProps[] { new EntityProps("error", ex.Detail.InnerXml) };
	}
	// Access the retrieved properties of the dynamic entity.	
	risultato.Add( new EntityProps(prop.Name, accountRetrieved.new_friendlyname.Value.Value));
	risultato.Add( new EntityProps(prop.Name, accountRetrieved.new_ncasellediposta.Value.Value));
	risultato.Add( new EntityProps(prop.Name, accountRetrieved.new_nutenti.Value.Value));
	risultato.Add( new EntityProps(prop.Name, accountRetrieved.new_stato.Value.Value));
	
	if(risultato.Count==0)
		return new EntityProps[] { new EntityProps("error", "Error: cannot find Company")};
	return res;
}

Molto utile la costante ORG_UNIQUE_NAME che consente tramite javascript di fare riferimento alla company corrente.

Se si vuole infine scegliere più in profondità con la customizzazione si deve procedere in questo modo:

  • Esportare l’xml dell’entità da customizzare
  • Modificare l’xml
  • Importare l’xml modificato per modificare l’entità

In questo modo possiamo accedere a proprietà prima non visibili come ValidForCreateAPI o ValidForUpdateAPI per specificare se un attributo è valido in fase di creazione o update. Inoltre possiamo accedere a fetchXml per costruire query per l’utente o per l’organizzazione (consente anche di creare viste personalizzate), anche creabili con FetchXmlBuilder.

Un ottimo articolo che illustra la customizzazione con CRM è l’articolo di Marcello Fisicaro.

Leave a Reply

You must be logged in to post a comment.