30. Juni 2013 · von Steffen Nörtershäuser

Update: Document Search

In diesem Beitrag möchte ich ein kurzes Update zu der bereits vorgestellten Document Search bringen. Denn auch wenn der dort vorgestellte Lösungsansatz für die meisten Fälle reicht und gute Ergebnisse liefert, so gibt es doch noch Verbesserungsmöglichkeiten. Ein Problem, welches die Nutzerfreundlichkeit merklich einschränkt, ist die synchrone Implementierung der Webservice Aufrufe.

Nachteile der Synchronene Webservice Aufrufe

  • Der Browser reagiert bei einem synchronen Aufruf solange nicht, bis eine Antwort vom Server empfangen wurde. Während dieses Verhalten bei Suchanfragen mit nur wenigen Ergebnissen nicht stark auffällt, wird es problematisch bei größeren Suchanfragen. Da für jedes Dokument mehrere Webservice Aufrufe durchgeführt werden müssen, kann es hier passieren das der Browser einige Sekunden „einfriert“. Für den durchschnittlichen Nutzer kann es so wirken als sei der Brwoser abgestürzt. Dadurch wird Frustation bei dem Anwender ausgelöst.
  • Asynchrone Abfragen können in einigen Situationen schneller arbeiten als synchrone. Dies ist darin begründet das die CPU nicht auf die Antwort des Webservice warten muss. Dadurch kann die CPU des Clients bereits weiter arbeiten währrend der Server die Daten zurück an den Client sendet.

Beide Nachteile werden durch die asynchronen Abfragen behoben: Dem Nutzer kann eine deutlichere Meldung wie „Lädt…“ angezeigt werden und die Performance profitiert ebenfalls.

document_search_loading

Notwendige Änderungen
Der erste Schritt der Transformation zu einer asynchronen Implementierung ist die Umsetzung einer Ladeanzeige. Das Standard CRM Loading Gif, was sich auch im obigen Screenshot sehen lässt, findet sich unter der folgenden relativen Server URL: „/_imgs/AdvFind/progress.gif“. Mit diesem Wissen kann man nun eine Funktion schreiben welche eine Ladenachricht darstellt:

/** brief Zeigt die Ladenachricht an
  */
function showLoadingScreen() {
  var loadingHtml = "<tr><td class='LoadingRow' style='height:75px'></td></tr><tr><td class='LoadingRow'><img src='/_imgs/AdvFind/progress.gif'/></td></tr>";
  loadingHtml += "<tr><td class='LoadingRow'>Lädt...</td></tr>";

  $("#results").html(loadingHtml);
  $("#results").css("width", "100%");
}

Da der Nutzer nun darüber informiert wird dass der Browser arbeitet können die Webservice Aufrufe umgestellt werden. Auf SharePoint seite lässt sich dies relativ einfach bewerkstelligen. Die SPServices Bibliothek stellt hierfür einen einfachen Parameter „async“ bereit. Wird dieser auf true gesetzt führt die Bibliothek die Abfrage asychron aus. Der neue Aufruf sieht also wie folgt aus:

$().SPServices({
      operation: "QueryEx",
      webURL: WEB_URL,
      queryXml: queryXml,
      async: true,
      completefunc: function (xData, Status) {
            //Verarbeiten der Daten
      }
});

Da dieser Aufruf hier als Einstieg fungiert sind keine weiteren Änderungen nötig. Die CRM Seite verlässt sich jedoch stärker auf die Eigenschaften eines synchronen Aufrufs, sodas hier mehr Änderungen nötig sind. Zur Erinnerung: In der alten Implementierung wurden die Daten zu den Suchergebnissen in einer Schleife nach und nach abgefragt und durch die klare sequentielle Abarbeitung ist es einfach die Ergebnisse zuzuordnen.
Bei der neuen asynchronen Verarbeitung ist dies nicht mehr gegeben und auch die Reihenfolge in der die Antworten des Servers ankommen ist nicht vorhersehbar womit die Zuordnung erschwert wird. Hier muss nun mit Callback Funktionen gearbeitet welche die Daten zuordnen und weitere Daten abfragen. Wird eine solche Funktion bei der XrmServiceToolkit.Soap.Fetch Funktion des XrmServiceToolkit angegeben wird dieser Aufruf automatisch asynchron durchgeführt. Somit sieht die neue Abfrage der Dokumenten Speicherorte wie folgt aus:

/** brief Fragt die Entitätsdaten zu den gefundenen Pfaden ab
* param searchResultsGroupByPath Gefundene Pfade
* return Entitätsdaten
*/
function getEntityData() {
    //Alle Ergebnisse durchsuchen
    for (curPath in SEARCH_RESULTS_GROUP_BY_PATH) {
        //Pfad säubern und trennen
        var splittedPath = curPath.replace(WEB_URL, "").split("/");

        getDocumentLocation(curPath, splittedPath, 0, null);
    }
}

/** brief Ermittelt den SharePoint Dokumenten Speicherort zu einem Teilpfad
* param curPath Pfad nachdem gesucht wird
* param splittedPath Teilpfad der gesucht wird
* param splitIndex Index im Pfad
* param oldLocation Alter SharePoint Dokumenten Speicherort des vorherigen Teilpfad
* return SharePoint Dokumenten Speicherort, null wenn keiner gefunden wurde
*/
function getDocumentLocation(curPath, splittedPath, splitIndex, oldLocation) {
    if (splittedPath.length <= splitIndex) {
        DATA_LOADED_COUNT++;

        if (typeof oldLocation != "undefined" && oldLocation != null && oldLocation.attributes["regardingobjectid"] != null) {
                var entityData = new Object();
                entityData.logicalName = oldLocation.attributes["regardingobjectid"].logicalName;
                entityData.id = oldLocation.attributes["regardingobjectid"].id;
                entityData.name = oldLocation.attributes["regardingobjectid"].name;
                entityData.Path = curPath;

                ENTITY_DATA[ENTITY_DATA.length] = entityData;
        }

        checkLoadingFinished();

        return;
    }

    //Dokumenten Location ermitteln
    var fetchXml =
"<fetch mapping='logical' version='1.0'>" +
    "<entity name='" + DOCUMENT_LOCATION_ENTITY + "'>" +
        "<attribute name='" + DOCUMENT_LOCATION_ENTITY + "id'/>" +
        "<attribute name='regardingobjectid'/>" +
        "<attribute name='regardingobjecttypecode'/>" +
        "<filter type='and'>" +
                "<condition attribute='relativeurl' operator='eq' value='" + splittedPath[splitIndex] + "' />";
            if (oldLocation != null && typeof oldLocation != "undefined") {
                        fetchXml += "<condition attribute='parentsiteorlocation' operator='eq' value='" + oldLocation.id + "' />";
            }
        fetchXml += "</filter>" +
                "</entity>" +
        "</fetch>";

    //Dokumenten Location abfragen
    XrmServiceToolkit.Soap.Fetch(fetchXml, function (retrievedDocumentLocation) {
        if (retrievedDocumentLocation.length == 0 && oldLocation == null) {
                DATA_LOADED_COUNT++;
                checkLoadingFinished();
                return;
        }

        getDocumentLocation(curPath, splittedPath, splitIndex + 1, retrievedDocumentLocation[0]);
    });
}

Dem aufmerksamen Leser wird die Funktion „checkLoadingFinished“ aufgefallen sein die noch nicht näher erläutert wurde. Diese prüft ob alle notwendigen Daten geladen wurden und die Suchergebnisse angezeigt werden können. Der Quellcode der Funktion sieht folgendermaßen aus:

/** brief Prüft ob der Ladevorgang abgeschlossen ist
  */
function checkLoadingFinished() {
    var resultCount = 0;
    for (var curProp in SEARCH_RESULTS_GROUP_BY_PATH) {
        if (SEARCH_RESULTS_GROUP_BY_PATH.hasOwnProperty(curProp)) {
            ++resultCount;
        }
    }

    if (DATA_LOADED_COUNT >= resultCount) {
        //Alte Pfade zwischenspeichern
        for (curPath in SEARCH_RESULTS_GROUP_BY_PATH) {
            TOTAL_SEARCH_RESULTS_PATH[curPath] = SEARCH_RESULTS_GROUP_BY_PATH[curPath];
        }

        if (ENTITY_DATA.length < PAGING_COUNT && RELEVANT_RESULT_COUNT >= PAGING_COUNT) {
            PAGING_START += PAGING_COUNT;
            loadNextPageData();
            return;
        }

        finishQuery();

    }
}

Wie hier zu sehen ist, wird in dieser Funktion als erstes die Anzahl an Suchergebnissen ermittelt. Da hier ein Objekt als assoziatives Array genutzt wird kann man nicht einfache eine Property wie length nutzen sondern muss die Properties des Objektes zählen. Ist dies erfolgt kann durch den Zähler „DATA_LOADED_COUNT“ geprüft werden ob der Ladevorgang abgeschlossen ist. Nun werden innerhalb der Funktion „finishQuery“ die Daten wie gewohnt angezeigt.
Die if-Abfrage welche eine Paging Abfrage beinhaltet kann hier ignoriert werden. Darauf werde ich in einem zukünftigen Artikel eingehen.

Fazit
Durch die asychrone Verarbeitung ist die Nutzerfreundlichkeit deutlich erhöht da der Browser nicht mehr „einfriert“. Dieses hier vorgestellte Update lässt sich als Solution, wie auch beim vorhigen Artikel, auf Codeplex finden.



Diesen Blogeintrag bewerten:


Haben Sie Fragen zu diesem Artikel oder brauchen Sie Unterstützung?

Nehmen Sie mit uns Kontakt auf!

Wir unterstützen Sie gerne bei Ihren SharePoint-Vorhaben!


Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Kontakt
Lassen Sie sich von uns beraten
Wir freuen uns über Ihr Interesse an unseren Leistungen. Hinterlassen Sie
uns Ihren Namen, Ihre Telefonnummer und E-Mail Adresse – wir melden
uns schnellstmöglich bei Ihnen.
Kontakt aufnehmen