Über Silvan Gehrig

WinRT. Mobile. Touch. Innovation. Technology. Namics.

How to Deploy Mobile Channels on SharePoint 2013

SharePoint 2013 bietet neu Mobile Channels an, welche die Device-gerechte Darstellung der Seiten auf verschiedene Mobile-Plattformen gewährleisten. Die Funktionalität sowie die Anwendung der Device Channels kann im Internet beispielsweise unter (http://blogs.msdn.com/b/sharepointdev/archive/2013/05/07/optimizing-sharepoint-2013-websites-for-mobile-devices.aspx) nachgelesen werden.

Dieser Artikel beschreibt, wie Device Channels automatisiert in einem Feature deployed werden können.

Der Ansatz von Microsoft

Als Ausgangslage habe ich das Deployment der Device Channels im SmallBusinessWebsite Feature von Microsoft verwendet. Dieses bietet Deployment für Mobile-Device Channels:

Das Feature.xml im Ordner %ProgramFiles%\Common Files\microsoft shared\Web Server Extensions\15\TEMPLATE\FEATURES\SmallBusinessWebsite verlinkt fürs Deployment der mobilen Master-Pages aufs MobileChannel.xml File.

Das MobileChannel.xml File verlinkt dabei die Mobile-Device Typen (UserAgentSubstrings; z.B. iOS oder $FallbackMobileUserAgents;) mit eigens definierten Namen (ChannelAlias) und füllt diese Informationen in die Device Channels List:

In den SharePoint Settings finden sich diese Informationen unter Device Channels wieder (abweichend zur Xml-Definition oben wird im folgenden Bild der Namics Mobile Channel dargestellt):

Master Pages für Device Channels

Anhand des ChannelAlias werden die hier definierten User Agent Gruppierungen zu Master Pages verknüpft. Die Benutzerschnittstelle dafür sieht wie folgt aus
(abweichend zur Xml-Definition oben wird im folgenden Bild der Namics Mobile Channel dargestellt):

Die Masterpage selbst wird im SmallBusinessWebsite Feature über das gewohnte Module-Deployment in die Masterpage-Gallery eingespielt:

Offen bleibt in der Feature-Definition, wie die Zuweisung der Masterpage auf einen Device Channel vonstattengeht.

Zuweisung der Masterpage auf einen Device Channel

Im SharePoint 2013 geschieht diese Zuweisung der Masterpage auf einen Device Channel über das Mappings-File __DeviceChannelMappings.aspx. Dieses liegt im MasterPage Katalog unter _catalogs/masterpage/ und enthält die Mappings im XML Format:

Beim SmallBusinessWebsite Feature kommt gemäss Feature-Definition der Microsoft.SharePoint.Publishing.SmallBusinessWebsiteFeatureHandler Receiver von Microsoft zum Einsatz, welcher das Mappings-File für die Site anlegt und konfiguriert.

Aufgrund der Komplexität des SmallBusinessWebsiteFeatureHandlers, habe ich hier das PublishingMobile Feature analysiert. Dieses deployed ebenfalls Mobile Channels:

Microsoft verwendet beim PublishingMobile Feature den Microsoft.SharePoint.Publishing.Mobile.FeatureHandler Receiver. Nach einigen Nachforschungen stellte ich fest, dass für die Zuweisung der Masterpage auf einen Device Channel Microsoft-interne (d.h. internal) Klassen wie die Microsoft.SharePoint.Publishing.Mobile.MasterPageMappingsFile zum Einsatz kommen:

Dies erschwert das automatische Deployment der Mobile Channels, da die entsprechenden Features nachgebaut werden müssen. Wesentliche Punkte dabei sind in folgenden Methoden abgedeckt:

  • Microsoft.SharePoint.Publishing.Mobile.MappingsFile<T>.UpdateFile()
    Channel Mappings File in die Master Page Gallery eintragen.
  • Microsoft.SharePoint.Publishing.Mobile.MappingsFile.SaveNoMobileMappingsInfoToRootWeb()
    Root Web Property für die Mobile Channels Deaktivierung (__NoMobileMapping) entfernen.

Putting it all together

Der folgende Code enthält den Receiver als Code-Schnippsel:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
/// <summary>
/// Initializes a new instance of the Microsoft.SharePoint.SPFeatureReceiver class.
/// </summary>
public class MobileChannelReceiver : SPFeatureReceiver
{
    private static readonly SPContentTypeId DeviceChannelMappingsContentTypeId = new SPContentTypeId("0x010100FDA260FD09A244B183A666F2AE2475A6");

    private const string mDeviceChannelFile = "__DeviceChannelMappings.aspx";
    private const string mMasterPageLibPath = "_catalogs/masterpage/";
    private const string mSiteCollectionUrlStart = @"href=""";
    private const string mSiteCollectionUrlReplacement = mSiteCollectionUrlStart + "~sitecollection";

    /// <summary>
    /// Deploys the mobile channel file into the Master Page Gallery. The existing
    /// file will be replaced with the file from the current feature.
    /// </summary>
    /// <param name="properties">An SPFeatureReceiverProperties object that represents the properties of the event.</param>
    public override void FeatureActivated(SPFeatureReceiverProperties properties)
    {
        using (SPWeb web = ((SPSite) properties.Feature.Parent).OpenWeb())
        {
            RegisterChannelFile(
                web,
                properties.Definition.RootDirectory + @"" + mDeviceChannelFile,
                mDeviceChannelFile);
        }
    }


    private void RegisterChannelFile(SPWeb rootWeb, string fullFilePathOfNewFile, string masterPageFileNameToReplace)
    {
        SPFile fileMasterpage = rootWeb.GetFile(mMasterPageLibPath + masterPageFileNameToReplace);

        if (fileMasterpage != null && fileMasterpage.Exists)
        {
            TryCheckedIn(fileMasterpage);
            TryCheckedOut(fileMasterpage);

            // patch file content with the root web url
            string mobileDeploymentFileRaw = File.ReadAllText(fullFilePathOfNewFile);
            string mobileDeploymentFile = mobileDeploymentFileRaw.Replace(
                mSiteCollectionUrlReplacement,
                mSiteCollectionUrlStart + rootWeb.ServerRelativeUrl);

            // Source: Microsoft.SharePoint.Publishing.Mobile.MappingsFile<T>::UpdateFile()
            // save patched content into server-side file library
            fileMasterpage.SaveBinary(Encoding.Unicode.GetBytes(mobileDeploymentFile));

            // update content type if has been set to a wrong one
            SPListItem item = fileMasterpage.Item;
               
            if (!DeviceChannelMappingsContentTypeId.IsParentOf((SPContentTypeId)item[SPBuiltInFieldId.ContentTypeId]))
            {
                item[SPBuiltInFieldId.ContentTypeId] = DeviceChannelMappingsContentTypeId;
                item.Update();
            }
               
            // accept changes and checkin
            fileMasterpage.CheckIn(string.Empty, SPCheckinType.MajorCheckIn);

            // approve if required
            SPModerationInformation moderationInformation = fileMasterpage.Item.ModerationInformation;

            if (moderationInformation != null && moderationInformation.Status == SPModerationStatusType.Pending)
            {
                fileMasterpage.Approve(string.Empty);
            }

            // activate custom master page channel mappings on web level
            // Source: Microsoft.SharePoint.Publishing.Mobile.MappingsFile<T>::SaveNoMobileMappingsInfoToRootWeb()
            string noMobileMappingKey = GetNoMobileMappingKey(rootWeb);
            if (rootWeb.AllProperties.ContainsKey(noMobileMappingKey))
            {
                rootWeb.AllProperties.Remove(noMobileMappingKey);
                rootWeb.Update();
            }
        }
    }

    private static string GetNoMobileMappingKey(SPWeb web)
    {
        return string.Format(CultureInfo.InvariantCulture, "__NoMobileMapping{0}", new object[]
        {
            web.ID.ToString("N")
        });
    }

    private bool TryCheckedOut(SPFile toCheckOut)
    {
        try
        {
            if (toCheckOut.Level != SPFileLevel.Checkout)
            {
                toCheckOut.CheckOut();
            }
        }
        catch (Exception ex)
        {
            LOG.Current.TraceUnexpected(
                CommonLibErrorCodes.EnsureApplicationFeatureReceiver,
                () => ex.Message);
            return false;
        }
        return true;
    }

    private bool TryCheckedIn(SPFile toCheckIn)
    {
        try
        {
            if (toCheckIn.CheckOutType != SPFile.SPCheckOutType.None)
            {
                toCheckIn.CheckIn("checked in by feature activation");
            }
            return false;
        }
        catch (Exception ex)
        {
            LOG.Current.TraceUnexpected(
                CommonLibErrorCodes.EnsureApplicationFeatureReceiver,
                () => ex.Message);
        }
        return true;
    }
}

Dafür wird das __DeviceChannelMappings.aspx Mappings File wie folgt deployed:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<%@ Reference VirtualPath="~CustomMasterUrlForMapping0" %><%@ Reference VirtualPath="~CustomMasterUrlForMapping1" %><%@ Page Language="C#" Inherits="Microsoft.SharePoint.Publishing.Internal.WebControls.MappingsFileBasePage" %><html xmlns:mso="urn:schemas-microsoft-com:office:office" xmlns:msdt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882"><%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<head>
<!--[if gte mso 9]><SharePoint:CTFieldRefs runat=server Prefix="mso:" FieldList="FileLeafRef"><xml>
<mso:CustomDocumentProperties>
<mso:ContentTypeId msdt:dt="string">0x010100FDA260FD09A244B183A666F2AE2475A6</mso:ContentTypeId>
</mso:CustomDocumentProperties>
</xml></SharePoint:CTFieldRefs><![endif]-->
</head><body><mappings>
  <mapping>
    <channelAlias>NamicsMobileChannel</channelAlias>
    <masterUrl href="~sitecollection/_catalogs/masterpage/SP2013Mobile.Master" token="~sitecollection/_catalogs/masterpage/SP2013Mobile.master" />
  </mapping>
  <defaultChannelMapping>
    <siteMasterUrl token="~sitecollection/_catalogs/masterpage/SP2013.Master" href="~sitecollection/_catalogs/masterpage/SP2013.Master" />
    <systemMasterUrl token="~sitecollection/_catalogs/masterpage/seattle.master" href="~sitecollection/_catalogs/masterpage/seattle.master" />
    <alternateCssUrl token="" href="" />
    <themedCssFolderUrl token="" href="" isthemeshared="false" />
  </defaultChannelMapping>
</mappings></body></html>

Fazit

SharePoint 2013 bietet mit den neuen Mobile Channels einen effizienten Weg, um die Seite Mobile-tauglich zu gestalten. Das automatisierte Deployment via Features ist im Moment noch nicht durchgehend gelöst, da die API fast ausschliesslich als internal markiert ist.  Hier muss Hand angelegt und mit einem entsprechenden Feature Receiver die Deployment-Automatisierung nachgerüstet werden.

Display SharePoint 2013 Followed items in a WebPart

SharePoint 2013 bietet die Möglichkeit, verschiedenen Seiten und Dokumenten zu folgen. Hierfür wird die MySite benötigt, welche als Site Collection in der Central Administration angelegt wird. Die Details zum Setup hat Microsoft unter http://technet.microsoft.com/en-us/library/ee624362.aspx beschrieben.

Dieser Artikel beschreibt wie die Followed Sites aus SharePoint in einen eigenen WebPart integriert werden können.

Warten auf die SP.SOD Klasse
Sämtliche Features der MySite werden über JavaScript / AJAX Calls angeboten. Daher ist hier der erste Ansatz, sich mit den Client-Side SharePoint Komponenten auseinander zu setzen. Dabei fällt als erstes die SP.SOD Klasse auf: Diese bietet On-Demand JavaScripting an: Somit ist es möglich, Funktionalitäten im Nachhinein zu laden und auf dessen Bereitstellung zu warten:

// Make sure SP.ClientContext is available
SP.SOD.executeFunc('sp.js', 'SP.ClientContext', function () {
    // Make sure SocialFollowingManager is available
    SP.SOD.executeFunc('userprofile', 'SP.Social.SocialFollowingManager', function() {
        // Do something when SocialFollowingManager is available
    });
});

Die executeFunc wartet, bis die angegebene Funktion (z.B. SP.ClientContext) geladen wurde und ruft anschliessend den bereitgestellten Funktionszeiger auf. Dieses Verhalten kann so verschachtelt werden, bis alle die benötigten Klassen mit Sicherheit geladen wurden.
Im Code-Schnippsel oben wird also gewartet, bis der SP.Social.SocialFollowingManager auf dem Client-Browser verfügbar ist.

Daten mit SP.Social.SocialFollowingManager Abfragen
Zum Abfragen der MySite  Daten wird die SocialFollowingManager Klasse verwendet. Auf dieser ist für unseren Case die getFollowed Methode interessant. Diese gibt als Resultat SocialActors zurück, welche die für uns relevanten Followed Sites enthalten. Aufgrund des SocialActorTypes kann bestimmt werden, welche Resultate (für uns Sites) im Result-Array gelistet werden.

Der folgende Code initialisiert den SocialFollowingManager und ruft anschliessend die getFollowed Methode auf:

var clientContext = SP.ClientContext.get_current();
var socialManager = new SP.Social.SocialFollowingManager(clientContext);
           
var socialActor = new SP.Social.SocialActorInfo();
socialActor.set_actorType(SP.Social.SocialActorTypes.sites);
           
var futureObject =  { result: socialManager.getFollowed(socialActor) };
     
clientContext.executeQueryAsync(
    function() {
        // visualize content in futureObject
    });

Zum Schluss wichtig ist der Aufruf von executeQueryAsync: Erst dieser führt den Call auf den Server aus und lädt die Daten vom Server ins futureObject.

Das WebPart als Ganzes
Anschliessend können die Daten mit beliebigem Client-seitigen Code visualisiert werden. Das Code-Schnippsel enthält ebenfalls den WebPart-Rumpf sowie die für ASP.NET benötigten Komponenten fürs Rendering.

[ToolboxData("<{0}:FavoritesWebPart runat=server></{0}:FavoritesWebPart>")]
public class FavoritesWebPart : System.Web.UI.WebControls.WebParts.WebPart
{
    #region Private Members

    private const string FavoritesContainerId = "fav";

    // Source:
    //  -> MSDN: http://msdn.microsoft.com/en-us/library/jj679862.aspx
    //  -> BLOG: http://techmikael.blogspot.ch/2013/03/following-or-favorite-pages-in.html
    //  -> BLOG: http://anujabhojani.blogspot.ch/2013_03_01_archive.html
    //  -> BLOG: http://blog.lekman.com/2013/06/making-suite-bar-menu-dropdown-in.html (SOLUTION)
    private const string FavoritesScriptStart = @"<script type=""text/javascript"">
<!--
(function($) {

(function(strElementId){

    var namicsFollower = {
        result: null,
        show: function() {
            var element = $("
"#"" + strElementId);
            $.each(this.result, function(idx, site) {
                var followCtnr = $("
"<p />"");
                var imageCtnr = $("
"<span />"",
                    {
                        style: "
"height:16px;width:16px;position:relative;display:inline-block;overflow:hidden;"",
                        'class': "
"s4-clust ms-promotedActionButton-icon""
                    });
                imageCtnr.append($("
"<img />"",
                    {
                        src: "
"/_layouts/15/images/spcommon.png?rev=23"",
                        alt: "
"Follow"",
                        style: "
"position: absolute; left: -218px; top: -48px;""
                    }));

                followCtnr.append(imageCtnr);
                followCtnr.append($("
"<a />"",
                    {
                        href: site.get_uri(),
                        text: site.get_name(),
                        style: "
"vertical-align: middle""
                    }));
                element.append(followCtnr);
            });
        },
        loadData: function() {
            var clientContext = SP.ClientContext.get_current();
            var socialManager = new SP.Social.SocialFollowingManager(clientContext);
       
            var socialActor = new SP.Social.SocialActorInfo();
            socialActor.set_actorType(SP.Social.SocialActorTypes.sites);
       
            this.result = socialManager.getFollowed(socialActor);

            clientContext.executeQueryAsync(Function.createDelegate(this, this.show));
        }
    }

    // Make sure SP.ClientContext is available
    SP.SOD.executeFunc('sp.js', 'SP.ClientContext', function () {
        // Make sure SocialFollowingManager is available
        SP.SOD.executeFunc('userprofile', 'SP.Social.SocialFollowingManager', function() {
            namicsFollower.loadData();
        });
    });
})("
;
    private const string FavoritesScriptInit = @"'{0}'";
    private const string FavoritesScriptEnd = @"
);

})(jQuery);

//-->
</script>"
;
   

    #endregion

    #region Public Members

    public FavoritesWebPart()
    {
    }

    /// <summary>
    /// Lifecycle override method for the CreateChildControls State of the WebPart.
    /// </summary>
    /// <remarks>
    /// The method handles all exceptions.
    /// </remarks>
    protected override void CreateChildControls()
    {
        try
        {
            base.CreateChildControls();

            HtmlControl favContainer = new HtmlGenericControl("div");
            favContainer.ID = FavoritesContainerId;
            Controls.Add(favContainer);

            Controls.Add(new LiteralControl(FavoritesScriptStart));
            Controls.Add(new LiteralControl(string.Format(
                FavoritesScriptInit,
                favContainer.ClientID)));
            Controls.Add(new LiteralControl(FavoritesScriptEnd));
        }
        catch (Exception ex)
        {
            // TODO: Log exception here...
            throw;
        }
    }

    #endregion
}

Log-In Dialog mit WinRT

Die neue Windows 8 Oberfläche bietet eine klare Übersicht über Ihre Apps und den darin verwalteten Daten. Heute kommen grösstenteils End-User Applikation in den Genuss dieser Features, welche als „Active Tiles“ bekannt sind. Allerdings ist die übersichtliche Ansichten nicht nur für Home-Kunden interessant, sondern auch fürs Business bietet die Windows 8 Style UI Oberfläche, beispielsweise als Ergänzung zum Intranet, reichen Mehrwert.

Besonders solche Intranet-Portale sind häufig mit einem Zugang gesichert. Für solche Applikationen ist ein Log-In Dialog zwingend, falls nicht direkt mit den Windows Credentials (siehe http://msdn.microsoft.com/en-us/library/windows/apps/windows.system.userprofile.userinformation.aspx) gearbeitet werden kann.

Benutzerinformationen für die App

Um Log-In Informationen einzugeben, hat Microsoft im WinRT Framework den CredentialsPicker-Dialog (http://msdn.microsoft.com/en-us/library/windows/apps/Hh701247) entworfen. Über diesen standardisierten Dialog kann der Benutzer seine Credentials eingeben. Der Dialog sieht wie folgt aus:

Der folgende Code zeigt den Dialog an:

private async Task<bool> OpenLoginDialogAsync()
{
CredentialPickerOptions credPickerOptions = new CredentialPickerOptions();
credPickerOptions.Message = „Bitte geben Sie Ihren Namics LDAP login ein.„;
credPickerOptions.Caption = „Namics Login„;
credPickerOptions.TargetName = „https://know.namics.com„;
credPickerOptions.CredentialSaveOption = CredentialSaveOption.Unselected;
credPickerOptions.AuthenticationProtocol = AuthenticationProtocol.Basic;

var credPickerResults = await CredentialPicker.PickAsync(credPickerOptions);

if (credPickerResults.Credential != null)
{
// TODO: Store retrieved data into variables / business layer / …
return true;
}
return false;
}

Die Überschrift sowie die Log-In-Mitteilung an den Client lassen sich anpassen. Gegenwärtig besteht keine Möglichkeit, weitere Texte wie Labels oder Buttons zu konfigurieren.
Der Dialog wird vom Framework modal dargestellt. Falls ein anderes modales Fenster geöffnet wird (beispielsweise der Konfiguration-Dialog), schliesst sich der CredentialPicker automatisch. Es können ungewollte Side-Effects auftreten, welche auf die modale Darstellung zurück zu führen sind – dagegen hilft meist nur eine Nach-Implementation des Dialoges.

CredentialPicker einbinden

Der CredentialPicker soll beim Start der App eingebunden werden. Damit dies realisiert werden kann, bietet WinRT die Möglichkeit eines Splash-Screens (im Beispiel LogInPage genannt). Diese Page wird direkt im App.xaml.cs registriert:

protected override void OnLaunched(LaunchActivatedEventArgs args)
{
if (args.PreviousExecutionState != ApplicationExecutionState.Running)
{
LogInPage extendedSplash = new LogInPage(
args.SplashScreen,
(args.PreviousExecutionState == ApplicationExecutionState.Terminated));
Window.Current.Content = extendedSplash;
}
// Ensure the current window is active
Window.Current.Activate();
}

Zu beachten ist, dass der SuspensionManager mit dem Frame-Management bewusst weggelassen wurde. Andernfalls wäre es möglich, mit dem Back-Button zurück zur LogInPage zu navigieren. Ebenfalls ist so beim Öffnen der App eine unautorisierte Navigation zur letzten geöffneten Ansicht unterbunden.
Die LogInPage selbst ist von LayoutAwarePage abgeleitet. Die Implementation könnte wie folgt aussehen:

/// <summary>
/// The Splash Screen page that is displayed as first page of the app.
/// </summary>
public sealed partial class LogInPage : LayoutAwarePage
{
private bool _appWasTerminated;
public LogInPage(SplashScreen splash, bool appWasTerminated)
{
InitializeComponent();
Loaded  += OnLogInPageLoaded;
}
private async void OnLogInPageLoaded(object sender, RoutedEventArgs e)
{
if (await OpenLoginDialogAsync() /*  see code above */)
{
// TODO: perform validation of provided login data  here;
//       otherwise force user to re-enter  credentials.
}
else
{
Application.Current.Exit();
}
// TODO: load data here… (e.g.  call await DataContext::LoadDataAsync())
await NavigateToInitialPageAsync();
}
private async Task NavigateToInitialPageAsync()
{
// Create a Frame to act as the  navigation ctx and navigate to the first page
Frame rootFrame = new Frame();
Window.Current.Content = rootFrame;
// Associate the frame with a  SuspensionManager key
SuspensionManager.RegisterFrame(rootFrame, „AppFrame„);
if (_appWasTerminated)
{
// Restore the saved session state  only when appropriate
try
{
await SuspensionManager.RestoreAsync();
}
catch (SuspensionManagerException sme)
{
// Something went wrong restoring  state.
// Assume there is no state and  continue
// TODO: Log error here…
}
}
if (rootFrame.Content == null)
{
   // When the navigation stack isn’t  restored navigate to the first page,
            // configuring the new page by  passing required information as a
            // navigation parameter
if (!rootFrame.Navigate(typeof(ItemsPage), „AllGroups„))
{
throw new NavigationException(typeof(ItemsPage), „AllGroups„);
}
}
    // Ensure the current window is  active
Window.Current.Activate();
}
}
Sobald die LogInPage initialisiert und geladen wurde, stellt WinRT den CredentialsPicker dar. Anschliessend navigiert die Prozedur NavigateToInitialPageAsync zur ItemsPage, welche dann mit den Inhalten aus dem Intranet arbeitet.

Fazit

Das richtige Einbinden des CredentialsPickers gestaltete sich als Herausforderung. Es muss darauf geachtet werden, dass während des Log-Ins keine weiteren modalen Dialoge geöffnet werden, da ansonsten mit ungeahnten Side-Effects zu rechnen ist. Ebenfalls muss der CredentialsPicker auf einer Splash-Screen Page aufgerufen werden, um den Log-In zu erzwingen.
WinRT stellt mit dem CredentialsPicker ein einfaches Werkzeug für die Benutzerdaten-Eingabe zur Verfügung. Aufgrund dessen Einfachheit lässt sich der Dialog nicht allen Wünschen gerecht konfigurieren – vor allem sämtliche Texte sollten sich anpassen lassen.

Resource Management mit WinRT

Resource Management ist ein Werkzeug zur Übersetzung von Texten in unterschiedliche Sprachen. Vielfach werden nicht nur GUI-Texte übersetzt, sondern auch Fehler- und Statusmeldungen aus dem Programmcode. Dieser Artikel beschreibt, wie grundsätzlich das Resource-Management für das GUI funktioniert und wie eine Lokalisierung für Code-Komponenten implementiert werden kann.

GUI Lokalisierung

WinRT bietet in Bereich GUI-Lokalisierung bereits heute zahlreiche Features an. So können die Texte im Ordner [WinRT GUI Projekt]\strings\[Sprach-Code] als resm-Files abgelegt werden. Die resm-Files sind im selben Format wie die aus .NET bekannten resx-Files aufgebaut.

Gui Lokalisierung

Eine Referenz über die Sprach-Codes und deren Bedeutung findet sich unter http://msdn.microsoft.com/en-US/library/windows/apps/hh965324.aspx.

Im WPF XAML-Code werden die Texte anschliessend mit x:Uid-Attribut eines beliebigen Controls referenziert. Der Compiler verknüpft anschliessend die Attribute des Controls mit den Texten aus dem referenzierten Sprach-File:

Die Sprachumschaltung zwischen den verschiedenen im \strings\ Ordner abgelegten Sprachen kann über die Systemsteuerung -> Sprache getestet werden.

Die WPF Lokalisierung ist in der MSDN unter http://msdn.microsoft.com/library/windows/apps/Hh965329(v=win.10).aspx detailliert beschreiben.

Der eigene Weg für Code-Lokalisierung

Weniger zahlreich sind die gebotenen Features im Bereich Code-Lokalisierung. Hier muss für eine wartbare Lösung selbst Hand angelegt werden, da, die aus dem .NET bekannten resx-Files, mit dem Compile Time Code Generator den Weg ins WinRT noch nicht gefunden haben. Daher ist hier ein möglichst .NET ähnlicher Weg anzustreben, da anzunehmen ist, dass entsprechende Features zu einem späteren Zeitpunkt noch nachgeliefert werden.

Dabei stellt sich die Frage, warum Code-Fragmente wie beispielsweise Fehlermeldungen überhaupt lokalisiert werden, da die englische Version der Meldung zumeist weit aussagekräftiger ist. Die Antwort darauf ist ähnlich wie bei der Benutzerschnittstelle: Auch Fehlermeldungen bergen das Risiko, bis zum Benutzer vorzudringen. Gerade bei wiederverwendeten Komponenten sollten die Texte also in separaten Files abgelegt werden, da Apps für verschiedene Sprachen und Regionen erstellt werden können.

Der einfachste Weg, die Code-Resourcen zu implementieren ist das Anlegen eines resm-Files und anschliessendes referenzieren über den Resource-Manager. Dies geschieht beispielsweise wie folgt:

Dies hat allerdings zum Nachteil, dass die Resources über Strings lose an die Eigenschaft im  resm-File gebunden werden. Bei einem Rename der Resource von resourceToFind auf resourceToRetrieve muss an allen Stellen im Programm-Code per Search & Replace der String „Resources/resourceToFind“ ersetzt werden. Falls nur eine Stelle vergessen wird, kann das Programm zur Laufzeit abstürzen.

Aus diesem Grunde ist es empfehlenswert, die Resourcen möglichst typisiert und automatisiert aus dem resm-File auszulesen und in ein Code-File als Property zu übertragen. Dies wird mittels Visual Studio .tt (T4) Templates erreicht. Sie finden eine Auflistung der T4-Template-Möglichkeiten unter http://msdn.microsoft.com/en-us/library/vstudio/dd820620.aspx . Das folgende C# Code-Schnipsel des Templates stellt dies sicher:

Die Resourcen werden aus dem angegebenen ReswFile geladen und damit anschliessend die C# Properties generiert. Die Variable resources muss im Vorfeld, wie im Resource-Manager Beispiel oben beschrieben, belegt werden:

Der einzige Nachteil dieser Lösung ist, dass der Entwickler den Template-Run manuell ausführen muss:

Mehr Informationen über das Resource-Management in WinRT können Sie unter http://msdn.microsoft.com/en-us/library/windows/apps/jj552947.aspx nachlesen.

Fazit

WinRT  bietet eine gute Lokalisierung im UI Bereich. Für die Lokalisierung des Programm-Codes muss sich der Entwickler selbst eine Lösung suchen, da die gebotenen Features noch nicht vollständig dem heutigen .NET Standard entsprechen.

Das Beispiel oben hat aufgezeigt, dass das Nachrüsten der Code-Lokalisierung keine grossen Aufwände mit sich bringt. Das Template-File einmalig mit minimalem Aufwand zu erstellen ist sicherlich eine lohnenswerte Investition – in der Hoffnung, dass beim nächsten WinRT Release eine ausgereifte Code-Lokalisierung mitgeliefert wird.

Logging für Windows Store Apps mit WinRT

Logging ist in der Programm-Entwicklung eine der wesentlichsten Methoden zur Fehlerverfolgung. So nimmt die Bedeutung von Logging mit den neuen .NET 4.5 Features (speziell async/await) zu, da im asynchronen Programmcontext bei unsachgemässer Implementation nicht deterministische Fehler „geschluckt“ werden können. Die Nachverfolgung solcher Fehler gestaltet sich, ohne Protokollierung, als äusserst schwierig.

Was WinRT bietet

Als rudimentäres Logging-Feature hat Microsoft zu diesem Zweck in WinRT die Event Tracing for Windows (ETW) Library für das Mobile- und Desktop-App Framework integriert. Dieses entfaltet vor allem für kleinere Entwicklungen seine Stärken, ist allerdings für den Einsatz von grösseren und komplexeren Lösungen nur bedingt geeignet. Es fehlen markante Features wie das Uploaden der Log-Files auf einen zentralen Server. Zusätzlich enthält das von Microsoft gebotene Beispiel Multithreading-Probleme, welche beim Save des Log-Files zum Tragen kommen können.

MetroLog kann Abhilfe schaffen

Das MetroLog Framework als OpenSource Projekt nimmt sich dieser Problematik an. Dieses basiert auf NLog und bietet neben der klassischen Logging-API auch den besagten Upload zum Server an. Weiterhin bietet MetroLog die Features zum Loggen von Daten ins ETW oder in eine lokale SQL-Lite Datenbank. Ein Beispiel zu MetroLog findet sich unter: https://github.com/mbrit/MetroLog/blob/master/Samples/ConsoleSample/Program.cs

Wunsch-Framework: log4net und noch mehr…

Persönliche habe ich eine Vorliebe für das log4net Framework. Dieses wurde bereits bei diversen .NET Projekten eingesetzt und ist entsprechend in der .NET Community etabliert. Ebenfalls setzen bereits bestehende interne Basis-Funktionalitäten log4net ein. Diese Funktionalitäten sollen nach und nach zu WinRT migriert werden, ohne hierbei ein grosses Rewrite zu generieren.

Leider exisitiert im Moment noch keine Implementierung des log4net Frameworks für WinRT. Aus diesem Grunde heisst die Lösung im Moment: Facade-Pattern der GoF. Mit Hilfe dieses Patterns, kann die effektive Logging-Implementierung im Nachhinein ohne Probleme ausgetauscht und so beispielsweise durch log4net Komponenten ersetzt werden.

Die Anwendung der Facade (Klasse Logger<T>) sieht wie folgt aus:

Durch die Facade kann ebenfalls die Logging-API minimiert werden. Im Beispiel werden direkt die Logger<ExampleClass>-Methoden aufgerufen. Diese loggen anschliessend die Mitteilung im Namen der Klasse ExampleClass. Der Compiler sorgt implizit dafür, dass genau eine Instanz des Templates Logger<ExampleClass> existiert, daher muss der Logger nicht explizit in jeder Anwender-Klasse instanziiert werden. Dank der Verwendung des Compiler-Features müssen auch keine Performance-Einbussen hingenommen werden.

Die Implementation der Logger<T> sieht im Prinzip sehr einfach aus:

Fazit

WinRT bietet im Moment erst rudimentäre Logging-Funktionalitäten. Dafür stehen Frameworks wie MetroLog zur Verfügung. Wer sich allerdings an ein zukunftssicheres API binden möchte, ist gut beraten, eine eigene Entwicklung anzustreben.

Im Beispiel oben wurde auf einfache Art und Weise gezeigt, wie das Logging mit WinRT mit den heutigen Mitteln zukunftssicher implementiert werden kann. Sicherlich ist die gebotene Implementierung aus aktueller Sicht noch ausbaufähig, doch bietet sie einen Grundstein für den späteren Einsatz einer mächtigeren Library.