SilverCRM cz. 2 – połączenie z Dynamics CRM.

Silverlight, aby spełnić wymaganie przed nim stawiane – uruchamianie się na różnych systemach operacyjnych (Windows, Linux, Mac) i w różnych przeglądarkach (IE, Firefox, Safari) – został zbudowany niejako obok .NET Framework. Fundamentem .NET Framework jest CLR. Silverlight został wyposażony w specjalne środowisko uruchomieniowe oraz w specjalny zbiór klas, aby móc spełnić tym wymaganiom. Jeśli chodzi o warstwę komunikacyjną to posiada on wbudowany WCF (Windows Communication Foundation). Chcąc skomunikować ze sobą Dynamics CRM oraz Silverlight będziemy łączyć ze sobą technologią ASP.NET oraz WCF. Jak dalej zostanie opisane – nie zawsze jest to, aż takie proste :)

Pierwszym krokiem jest skorzystanie z opcji dodania referencji do usługi. W przypadku starszych technologii w VS posiadaliśmy opcję Add Web Reference. Wraz z WCF dostaliśmy opcję Add Service Reference, którą można było zamienić na zwykłą Web Reference (wchodzą w dodatkowe ustawienia). W Silverlight nie mamy takiej opcji i musimy korzystać tylko z Service Reference – tak jak to jest pokazane na rysunku:

 

Skorzystanie z opcji dodania referencji do usługi spowoduje wyświetlenie okienka, w którym wprowadzić będzie trzeba adres, pod którym dostępna będzie usługa (np. http://localhost/MSCrmServices/2007/CrmService.asmx). Można tutaj również wskazać np. ścieżkę na lokalnym dysku gdzie znajduje się plik z definicją usługi (*.wsdl). Po odpowiednim skonfigurowaniu i wygenerowaniu namiastki zostanie w projekcie utworzony katalog Service References gdzie znajdziemy kod źródłowy stworzonej namiastki.

 

Oprócz kodu namiastki zostanie wygenerowany plik konfiguracyjny, który będziemy mogli wykorzystać w czasie tworzenia instancji namiastki. Zamiast pisać kod i wskazywać, z których wiązań i w jakiej konfiguracji należy korzystać można te wartości wskazać w pliku konfiguracyjnym i w czasie tworzenia instancji namiastki na ten plik się powoływać. Przykładowy plik konfiguracyjny wygląda następująco:

<configuration>
    <system.serviceModel>
        <bindings>
            <basicHttpBinding>
                <binding name="CrmServiceSoap" maxBufferSize="2147483647" maxReceivedMessageSize="2147483647">
                    <security mode="None" />
                </binding>
            </basicHttpBinding>
        </bindings>
        <client>
            <endpoint address="
http://localhost/MSCrmServices/2007/CrmService.asmx"
                binding="basicHttpBinding" bindingConfiguration="CrmServiceSoap"
                contract="CrmSdk.CrmServiceSoap" name="CrmServiceSoap" />
        </client>
    </system.serviceModel>
</configuration>

Warto tutaj zwrócić uwagę na nazwę końcówki (endpoint), która w pokazanym pliku jest ustawiona na CrmServiceSoap. W momencie tworzenia instancji namiastki będzie można wskazać na jaką konfigurację WCF musi się powołać, aby poprawnie namiastkę zainicjować.

Warto tutaj zwrócić uwagę na nazewnictwo klas wygenerowanym z pliku *.wsdl usługi CRM. W czasie tworzenia namiastki poprzez zwykłe Web Reference dostawaliśmy w efekcie klasę CrmService. Tutaj otrzymujemy CrmServiceSoapClient. Inne klasy takie jak account, contact, reprezentujące obiekty CRM generowane są bez żadnych zmian.

Tworzenie instancji namiastki jest następujące:

CrmServiceSoapClient _client = new CrmServiceSoapClient("CrmServiceSoap");

gdzie CrmServiceSoapClient jest klasą wygenerowaną przez WCF, natomiast argumentem konstruktora tej klasy jest wskazanie konfiguracji końcówki z pliku konfiguracyjnego również wygenerowanego przez WCF.

Silverlight umożliwia wywoływanie tylko i wyłącznie metod asynchronicznych. Zatem w namiastce nie mamy metod CrmService takich jak Execute, Create, Update. Mamy natomiast metody ExecuteAsync, CreateAsync, itd. Wszelkie operacji przy pomocy Silverlight wykonywane są asynchronicznie !!! Mając to na uwadze po utworzeniu namiastki trzeba obsłużyć zdarzenia zakończenia wykonywania operacji asynchronicznej. Przykładowy kod wygląda tak:

_client.RetrieveMultipleCompleted += new EventHandler<RetrieveMultipleCompletedEventArgs>(OnClientRetrievedMultiple);
_client.ExecuteCompleted += new EventHandler<ExecuteCompletedEventArgs>(OnClientExecuteCompleted);

, gdzie metody OnClientRetrievedMultiple oraz OnClientExecuteCompleted są zdefiniowane następująco:

void OnClientExecuteCompleted(object sender, ExecuteCompletedEventArgs e) {}

void OnClientRetrievedMultiple(object sender, RetrieveMultipleCompletedEventArgs e) {}

Aby wiedzieć do jakiej organizacji się połączyć do tej pory (tzn. korzystając z Web Reference) otrzymywaliśmy klasę CrmAuthenticationToken, który umożliwiał wskazanie do której organizacji chcemy się połączyć, w jaki sposób chcemy dokonać uwierzytelnienia, itd. W przypadku WCF oczywiście otrzymujemy klasę CrmAuthenticationToken w namiastce jednakże klasa CrmServiceSoapClient nie posiada właściwości, która umożliwia ustawienia i wykorzystanie tokenu. Aby poprawnie połączyć się z usługą CRM musimy mieć możliwość dołączenia do żądań SOAP nagłówka – musimy mieć metodę GenerateAuthenticationHeader, którą mamy jak piszemy skrypt uruchamiany, np. na zdarzeniu OnLoad formatki CRM. Metodę tą możemy zbudować korzystając z kanały komunikacyjnego, z którego korzysta WCF. Musimy ręcznie dołączyć nagłówek do wiadomości wychodzących od naszego komponentu:

MessageHeader header = MessageHeader.CreateHeader("CrmAuthenticationToken",
                  "http://schemas.microsoft.com/crm/2007/WebServices",
                  "",
                  new CrmAuthenticationTokenSerializer(0, orgname, null));

OperationContext.Current = new OperationContext((IContextChannel)_client.InnerChannel);
OperationContext.Current.OutgoingMessageHeaders.Add(header);

Klasa CrmAuthenticationTokenSerializer jest naszą własną klasą dziedziczącą po XmlObjectSerializer:

    public class CrmAuthenticationTokenSerializer : XmlObjectSerializer
    {
        int _authType;
        string _organizationName;
        string _callerId;

        public CrmAuthenticationTokenSerializer (int authType, string organizationName, string callerId)
        {
            _authType = authType;
            _organizationName = organizationName;
            _callerId = callerId;
            if (_callerId == null)
            {
                _callerId = "00000000-0000-0000-0000-000000000000";
            }
        }


        public override void WriteStartObject(XmlDictionaryWriter writer, Object graph)
        {
        }

        public override void WriteObjectContent(XmlDictionaryWriter writer, Object graph)
        {
            string authToken = string.Format("<AuthenticationType xmlns='http://schemas.microsoft.com/crm/2007/CoreTypes'>

            {0}</AuthenticationType><OrganizationName xmlns='http://schemas.microsoft.com/crm/2007/CoreTypes'>

            {1}</OrganizationName><CallerId xmlns='http://schemas.microsoft.com/crm/2007/CoreTypes'>

            {2}</CallerId>", _authType, _organizationName, _callerId);

            writer.WriteRaw(authToken);
        }

        public override void WriteEndObject(XmlDictionaryWriter writer)
        {
        }


        public override bool IsStartObject(XmlDictionaryReader reader)
        {
            return true;
        }

        public override Object ReadObject(XmlDictionaryReader reader, bool verifyObjectName)
        {
            return null; 
        }

    }

Mając tak zdefiniowaną klasę możemy przystąpić do wywoływania metod usługi CRM. Jednakże w czasie wywołania najprostszego żądania, np. WhoAmIRequest dostaniemy błąd związany z komunikacją pomiędzy domenami. Dlaczego tak się dzieje ? O tym w kolejnym wpisie. Tam również kilka informacji na temat błędów związanych z metodami Retrieve oraz RetrieveMultiple.