SharePoint: Resource-Strings sicher auslesen

Resource-Files sind eine feine Sache. Sie erlauben bekanntermaßen die Lokalisierung eines Projektes, indem sie Satzfetzen in sprachabhängige XML-Dateien (.resx) auslagern und unter einem gemeinsamen Key zugreifbar machen.

Um Strings per C# (serverseitig) aus Resources/MeinSchnuckeligesResourceFile.resx auszulesen, könnte man z.b. eine einfache Methode schreiben:

public static string GetResString(string key)
   {
      // Return the value of a language resource
      // in the current language.
      string str = "$Resources:"+key;
      return SPUtility.GetLocalizedString(str,
                "MeinSchnuckeligesResourceFile",
                (uint) CultureInfo.CurrentUICulture.LCID).Trim();
    }

So weit so gut.
Manchmal kommt es vor dass Kunden oder andere unbedarfte Personen selbst Hand an die Resource-Files legen dürfen, und es kommt auch vor dass Strings Platzhalter der Form {0}, {1}… enthalten, die später mit Werten ersetzt werden. Wenn ein Platzhalter im String referenziert wird, der nicht übergeben wurde fliegt eine Exception. Es wäre also sinnvoll auf das vorhandensein von solchen nicht abgedeckten Platzhaltern zu testen, dafür dient nachfolgende Methode, die im Prinzip wie String.Format() funktioniert, nur dass sie anstatt des strings den Resource-Key übergeben bekommt:

/// <summary>
/// Retuns a formated string. Unse this when the resource string 
/// contains a {0} placeholder. 
/// Returns the unformatted string if a placeholder refers to 
/// a non existent parameter to avoid an exception
/// </summary>
/// <param name="key">the key in resource file</param>
/// <param name="param">params like in string.format</param>
/// <returns>formatted string. in case of an error, unformatted string.</returns>

public static string GetFormatedResString(string key, params object[] arguments)
{
   string result = String.Empty;
   string fmt = GetResString(key);

   // check if format string does not refer to a non-given argument
   // Because we don't want exceptions just because of wrong resource strings
   var matches = from Match match in Regex.Matches(fmt, @"\{([0-9]+)\}")
   select match.Groups[1].Value;

   bool dirty = false;
   foreach (string mat in matches)
   {
      // if {} placeholder refers to an argument which is not there -> dirty
      if (int.Parse(mat) > arguments.Count() - 1)
      {
         dirty = true;
      }
   }

   if (!dirty)
   {
      // return formatted string
      result = String.Format(fmt, arguments);
   }
   else
   {
      // return unformatted string
      result = fmt;
   }
   return result;
}

Keine Rocket-Science, aber manchmal ganz hilfreich.

Advertisements

Kein Sommertag ohne Zippi

Sommer. Es ist heiß im Büro.

Mir war es bislang leider nicht vergönnt, den Tag in einem adäquat klimatisierten Büro zu verbringen, in keinem Projekt. Bei fast 30° klebt die Haut, das Hirn ist quasi im Standby, die Produktivitätskurve ist nahe der Nulllinie.

Was fehlt ist Luft, Abkühlung, Wind, ach was sage ich, Sturm, Gischt, Regen! Zumindest für den leichten bis mittelstarken Luftzug tut ein profaner Ventilator gute Dienste.

Ich habe mich für einen Zippi von der Firma Vornado entschieden. Gekauft für 22€ bei Amazon.

Warum der?

Ich habe so einen Ventilator vor Jahren mal bei einem Projektkollegen auf dem Schreibtisch stehen sehen, und fand das Teil sofort bemerkenswert. „Der ist ja niedlich!“

Die „Rotorblätter“ sind aus Stoff und damit weich. Man kann sich also nicht daran verletzen. Es tut nicht mal weh wenn man auf höchster Stufe versehentlich dagegen kommt. Dadurch kann man sich den Sicherheitkorb sparen und der Luftzug wird leiser – genial!

Leise ist er wirklich und selbst auf Stufe 1 von 2 macht er genug wind um den Arbeitstag mit voller Hirnleistung zu überstehen. Stufe 2 ist mir persönlich zu windig. Und dabei ist der obere Teil worin sich der Motor befindet nicht größer als eine einförmige Apfelsine.

zippi_black_hero1

Hach, ist er nicht süß?

Wegen der weichen Flügen und der Tatsache, dass sich der Motor nach unten in den Ständer drehen lässt, ist der Kleine auch gut to transportieren, wichtig wenn man im unklimatisierten Budget-Hotelzimmer noch weiter coden möchte und das Gewitter auf sich warten lässt…

Ich bin jetzt jedenfalls auch noch bei 28°C + produktiv 🙂

Hier geht’s zur Produktseite:
Vornado Zippi Tischventilator, schwarz, 701177

 

SharePoint 2013: „Append Changes“ Textfelder auflösen

„Append Changes“-Felder sind praktisch

Textfelder, die mit dem „Append Changes“ Attribut ausgestattet sind, sind prinzipiell eine feine Sache. Jeder Eintrag wird mit Datum und eintragendem Benutzer zu dem bereits bestehenden Text hinzugefügt, der alte Eintrag ist nicht veränderbar. Großartig für Kommentarfelder oder jegliche Form von Journalen.

…aber ungünstig in der Listendarstellung.

In der Listenansicht wird so ein Feld jedoch nur als „View Entries“-Link auf die Leseansicht des Listeneintrags dargestellt. Unschön und vor allem fürchterlich unpraktisch. Das liegt vor allem daran, dass die Änderungen als Versionen gespeichert werden. Um alle Einträge zu bekommen müssen also alle Versionen des betreffenden Feldes gelesen und dargestellt werden. SharePoint liefert solch ein Control leider nur für die View- und Edit-Ansicht eines Items, nicht jedoch für die Listen-Ansicht.

Die Lösung

Doch es gibt Abhilfe. Seit der Version 2013 von SharePoint existiert die Möglichkeit per JavaScript in die Darstellung einzelner Felder in der Listenansicht einzugreifen. JSLink. Das ist nichts weiter als der Link zu einem JavaScript-File die dem ListView Webpart oder dem List-Schema.xml mitgegeben werden kann. In diesem Script lassen sich dann callback-Funktionen definieren für einzelne Felder definieren, die den Inhalt der Spalte liefern. Dieser Inhalt kann alles sein, was der Browser verarbeitet, meistens HTML und Javascript.

Aus diesem Grunde habe ich ein paar sinnvolle JavaScript Funktionen geschrieben, die man für sein JSLink-Script verwenden kann. Es handelt sich im Prinzip um zwei Funktionen, die die Änderungshistorie verschieden darstellen:

Als Popup, welches aufklappt wenn man mit der Maus darüberfährt:

bild1

Oder direkt in der betreffenden Zelle.

Der Code

Hier nun erstmal der Code:


// ------------------------------------------------------------------------------------------
// Shows the content of the current &amp;quot;append only&amp;quot; text field in a popup
function ShowVersionedPopup(ctx) {
    var listUrl = ctx.listUrlDir;
    var listName = ctx.ListTitle;

    var id = &amp;quot;FB_Mo_&amp;quot; + ctx.CurrentFieldSchema.ID + ctx.CurrentItem[&amp;quot;ID&amp;quot;];
    html = &amp;quot;&amp;lt;img id='IMG&amp;quot; + id + &amp;quot;' src='/_layouts/15/images/myproject/moretext.png' onMouseOver=\&amp;quot;$('#&amp;quot; + id            + &amp;quot;').css('visibility','visible')\&amp;quot; onMouseOut=\&amp;quot;$('#&amp;quot; + id + &amp;quot;').css('visibility','hidden')\&amp;quot; /&amp;gt;&amp;quot;
           + &amp;quot;
&amp;lt;div id='&amp;quot; + id + &amp;quot;' style='visibility:hidden; position:absolute; padding:5px;&amp;quot;            + &amp;quot;box-shadow: 10px 10px 29px 0px rgba(184,184,184,1); &amp;quot;            + &amp;quot;border: 2px solid #000000;&amp;quot;            + &amp;quot;background-color: white'&amp;quot;            + &amp;quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;quot;;
    GetVersions(listName, ctx.CurrentItem[&amp;quot;ID&amp;quot;], ctx.CurrentFieldSchema.Name, id);
    return html;
}

// ------------------------------------------------------------------------------------------
// Shows the content of the current &amp;quot;append only&amp;quot; text field directly in the list field
function ShowVersionedDiv(ctx) {
    var listUrl = ctx.listUrlDir;
    var listName = ctx.ListTitle;

    var id = &amp;quot;FB_Mo_&amp;quot; + ctx.CurrentFieldSchema.ID + ctx.CurrentItem[&amp;quot;ID&amp;quot;];
    html = &amp;quot;
&amp;lt;div id='&amp;quot; + id + &amp;quot;'&amp;gt;&amp;lt;/div&amp;gt;
&amp;quot;;
    GetVersions(listName, ctx.CurrentItem[&amp;quot;ID&amp;quot;], ctx.CurrentFieldSchema.Name, id);
    return html;
}
// ------------------------------------------------------------------------------------------
// Retreives all versions of a field of a specifiv List Item
// listName: Name or ID of the List
// itemID: ID of the ListItem
// internalFieldName: The internal Name of the field
// elementId: The ID of the HTML element (usually a div) where the HTML output should be pumped in
function GetVersions(listName, itemId, internalFieldName, elementId)
{
    var xmlData = &amp;quot;&amp;lt;soap:Envelope xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/' &amp;quot;     xmlData += &amp;quot;xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:xsd='http://www.w3.org/2001/XMLSchema'&amp;gt;&amp;quot;;
    xmlData += &amp;quot;&amp;lt;soap:Body&amp;gt;&amp;lt;GetVersionCollection xmlns='http://schemas.microsoft.com/sharepoint/soap/'&amp;gt;&amp;quot;;
    xmlData += &amp;quot;&amp;lt;strlistID&amp;gt;&amp;quot; + listName + &amp;quot;&amp;lt;/strlistID&amp;gt;&amp;lt;strlistItemID&amp;gt;&amp;quot; + itemId + &amp;quot;&amp;lt;/strlistItemID&amp;gt;&amp;lt;strFieldName&amp;gt;&amp;quot;
    xmlData += internalFieldName + &amp;quot;&amp;lt;/strFieldName&amp;gt;&amp;lt;/GetVersionCollection&amp;gt;&amp;lt;/soap:Body&amp;gt;&amp;lt;/soap:Envelope&amp;gt;&amp;quot;;

    $.ajax({
        url: &amp;quot;&amp;lt;pfad zum web&amp;gt;/_vti_bin/lists.asmx&amp;quot;, // &amp;lt;---- Hier Anpassen
        type: &amp;quot;POST&amp;quot;,
        dataType: &amp;quot;xml&amp;quot;,
        data: xmlData,
        complete: function (jqXHR) { GetVersionsSuccess(jqXHR, elementId, internalFieldName); },
        error: GetVersionsError,
        contentType: &amp;quot;text/xml; charset=\&amp;quot;utf-8\&amp;quot;&amp;quot;
    });

};

// ------------------------------------------------------------------------------------------
// Callback func in terms of successful webService operation
// result: the result SOAP envelope
// divId: The ID of the HTML element (usually a div) where the HTML output should be pumped in
// internalFieldName: The internal Name of the SPList field
function GetVersionsSuccess(result, elementId, internalFieldName) {
    var rTable = &amp;quot;
&amp;lt;table&amp;gt;&amp;quot;;
    $()
    //alert(result.esponseText);r
    if ($(result.responseXML).find(&amp;quot;Version&amp;quot;).length &amp;gt; 0) {
        $(result.responseXML).find(&amp;quot;Version&amp;quot;).each(function () {
            if ($(this).attr(internalFieldName) != &amp;quot;
&amp;lt;div&amp;gt;&amp;lt;/div&amp;gt;
&amp;quot;) {
                var vDate = $(this).attr(&amp;quot;Modified&amp;quot;);
                rTable += &amp;quot;
&amp;lt;tr&amp;gt;
&amp;lt;td style='vertical-align: top; padding-bottom: 10px; padding-right: 8px;'&amp;gt;&amp;quot; + $(this).attr(&amp;quot;Editor&amp;quot;).split(&amp;quot;#,#&amp;quot;)[1] + &amp;quot; (&amp;quot;
                       + vDate.substr(5, 2) + &amp;quot;/&amp;quot; + vDate.substr(8, 2) + &amp;quot;/&amp;quot; + vDate.substr(0, 4)
                       + &amp;quot;)&amp;lt;/td&amp;gt;
&amp;lt;td style='min-width:200px; vertical-align: top; padding-bottom: 10px;'&amp;gt;&amp;quot;
                       + lineEncode($(this).attr(internalFieldName)) + &amp;quot;&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;quot;;
            }
        });
        rTable += &amp;quot;&amp;lt;/table&amp;gt;
&amp;quot;;
        $('#' + elementId).append(rTable);
    }
    else {
        // No entries, hide the icon (if there is one)
        // it is assumed that the corresponding image has the id IMG+divId
        if ($('#IMG' + elementId).length &amp;gt; 0) {
            $('#IMG' + elementId).css(&amp;quot;visibility&amp;quot;, &amp;quot;hidden&amp;quot;);
        }
    }
}

// ------------------------------------------------------------------------------------------
// This fuction is called when an error in the request itself occurs
// (like 404 or timeout) - not when the webService returns an error
function GetVersionsError(result) {
    // just log it
    console.log(result);
}

function lineEncode(value) {
    //create a in-memory div, set it's inner text(which jQuery automatically encodes)
    //then grab the encoded contents back out.  The div never exists on the page.
    return value.replace('\n', '
').replace('\r','
');
}

Wie funktioniert es?

Die Versionen eines Feldes bekommt man mit dem SOAP Webservice lists.asmx und dessen Methode GetVersionsCollection. Dieser Webservice wird in der Funktion GetVersions() asynchron aufgerufen, das Ergebnis als HTML aufbereitet und per jQuery in das Containerelement (in diesem Fall ein <div>) hineingeschrieben.

Wie wird es benutzt?

Der obige Code ist wie gesagt Bestandteil des Scriptes, das per JSLink referenziert wird. An dem Script müssen folgende Dinge nach eigenen Bedürfnissen angepasst werden:

  • Der Pfad zum Icon (Zeile 8)
  • Der Pfad zum Web (Zeile 43)

Vor dem obigen Scriptblock muss noch der Aufruf erfolgen, bzw die Definition welche Felder durch welche Funktion dargestellt werden. Der folgende Block steht am Anfang des Script-Files der Block oben kommt einfach dahinter.

(function () {
    var fbListContext = {};
    fbListContext.Templates = {};

    fbListContext.Templates.Fields = {

        'KommentarAlsPopup' : {          // Interner Feldname
            'View': ShowVersionedPopup   // Als Popup darstellen
        },

        'KommentarInZelle': {            // Interner Feldname
            'View': ShowVersionedDiv     // Direkt in die Zelle schreiben
        }
    };

    SPClientTemplates.TemplateManager.RegisterTemplateOverrides(fbListContext);
})();

Dieser Codeblock steht am Anfang des JSLink Scriptes und definiert, welche Felder (interne Feldnamen!) durch welche Funktion dargestellt werden sollen. Die Funktonen ShowVersionedPopup und ShowVersionedDiv sind in dem ersten Codeblock definiert.

Danach muss noch das gesamte Script eingebunden werden, dafür gibt es verschiedene Möglichkeiten, die sich unter [1] nachlesen lassen.

Viel Spaß!

Weitere Infos

[1] JSLink Tutorial