Tag 1 der TechEd Europe 2014 ist vorbei und direkt nach dem ersten Tag, bzw. direkt nach der Keynote gab es viele Neuigkeiten über die berichtet werden muss 🙂 . Deshalb habe ich mir gedacht, dass ich mal ein Recap schreibe.

Hier sind die Neuigkeiten die mich in der Keynote am meisten begeistert haben.

Es gibt immer mehr Daten die zu verarbeiten sind (Jason Zander: „Explosion of Data„). In Zukunft werden es auch Daten sein, mit denen momentan noch nicht gerechnet wird. Wie z.B. Daten von Autos, Wearables usw. Diese Menge muss natürlich auch verarbeitet werden und da bietet sich die Cloud mit unbegrenzter Kapazität natürlich an.

Datenmenge der Zukunft

Datenmenge der Zukunft

Microsoft beschreibt sich selbst als die Productivity- and Platform-Company in der Mobile-First und Cloud-First Welt. Durch Produkte wie Office 365 und die Verfügbarkeit von Office auf allen große mobilen Betriebssystem (Windows, iOS, Android) kann von überall Content erstellt werden. Und dieser Content kann durch die Skalierbarkeit von Microsoft Azure verwaltet werden.

Mobile-First + Cloud-First

Mobile-First + Cloud-First

Windows 10

Windows 10 Preview und die „neuen Features“ davon

  • Nur noch eine Windows Core für alle Devices wie Windows auf PCs, auf Tablets, Windows Phone und die XBOX One
  • Neues altes Start Menü, halb Windows 7, halb Windows 8. Back to the roots 😀
  • Windows Store Apps können vergrößert und verkleinert werden (auch schon per Update in Windows 8.1 vorhanden)
  • Copy und Paste im Command Prompt per Shortcuts
  • Snap Windows in Multi-Monitor Arbeitsplätzen
  • Two-Factor Authentication über Bluetooth

Company Protection ermöglich es Dokumente als Unternehmensdaten zu speichern, somit ist es nur bedingt möglich Daten die als Unternehmensdaten gekenntzeichnet sind an die Öffentlichkeit (Facebook, Twitter, etc.) weiterzugeben.

Company Protection

Company Protection

BYOD, bring your own device, Support für Mitarbeitergeräte. Durch die Auswahl ob das Gerät ein Unternehmensgerät ist, können benutzerfreundlich Unternehmens-Policies und -Einstellungen bereitgestellt werden. Der Benutzer hat trotzdem eine OOTB-Experience.

OOTB Experience

OOTB Experience

Corporate Identity Login

Corporate Identity Login

Cloud Stuff

Laut Microsoft soll jedes On-Premises System mit der Cloud verbunden werden. Dafür gibt es auch schon Tools wie StorSimple. Außerdem sollte man die Server bzw. VMs nicht mehr „von Hand“ warten sondern stattdessen am besten eine eigene Cloud Umgebung mittels Azure Pack aufbauen.

Microsoft Azure Operational Insights, ein neues Tool um System-Administratoren das Arbeiten zu erleichtern. Das Tool bietet eine Übersicht aller Resourcen eines System, wie z.B. Speicherverbrauch, Change Tracking, uvm.
Die Preview ist verfügbar unter https://preview.opinsights.azure.com.

Azure Operational Insights

Azure Operational Insights

Azure Operational Insights

Azure Operational Insights

Microsoft Azure Pack, ab heute verfügbar in Update 4, stellt Dienste aus Microsoft Azure bereit, die in On-Premises Systemen benutzt werden können. Dabei wird dem Benutzer die selbe UI wie in Microsoft Azure bereitgestellt.
Ich persönliche liebe es, da man alles über Web einstellen kann und keinen Client für SCCM braucht. Außerdem kann man über PowerShell auch alle bekannte Microsoft Azure Commands benutzen.
Microsoft Azure On-Prem. Was braucht man mehr 🙂 .

Azure Pack

Azure Pack

Neue Datencenter für Microsoft Azure. Willkommen Australien 🙂 .
Genauer sind es jetzt 19 Datencenter, die über 600.000 Server beherbergen. Das sind doppelt so viele Regionen wie Amazon anbietet und 6-mal so viele wie Google anbietet.

Azure Datacenters

Azure Datacenters

Neue Virtual Machine Serie, die G-Serie, bringt mehr Power!
Bis zu 32 CPU-Kerne, 448 GB Ram und 6,5TB SSD.

Desweiteren haben die Features Azure Automation sowie Azure WebJobs nun den Status GA.

Für mehr Informationen empfehle ich die Keynote auf Channel9 🙂 .
http://channel9.msdn.com/Events/TechEd/Europe/2014/KEY01

Viele Grüße aus Barcelona!

Schön und aufregend war die Basta 2014.
Vielen Dank an die Zuhörer in meiner Session Authorize your Apps! – OAuth in SharePoint 2013 und natürlich auch an die Zuhörer die zu später Stunde noch in der Session Die Zukunft von SharePoint und Office 365 von Fabian Moritz waren, in der ich Demos zur neuen Office 365 API gezeigt habe.

Die Slides zu meiner Session gibt’s auf Speaker Deck und die Demos habe ich in GitHub bereitgestellt.
Viel Spaß damit! 😀

Authorize your Apps! – Oauth in SharePoint 2013

Demos

Die Zukunft von SharePoint und Office 365

Demos

Ich musste heute feststellen, dass das Öffnen von SiteCollections über die Site ID nicht immer die beste Lösung ist. Natürlich ist es die schnellste, das bleibt mir als Entwickler erstmal im Gedächtnis. Nur manchmal ist es nicht möglich oder sinnvoll eine SiteCollection über die ID zu öffnen.
Doch erstmal zum Problem…

Bei mir trat ein Problem beim Arbeiten mit Bilder und Links auf. Links haben auf eine falsche URL gezeigt und Bilder wurden nicht geladen, auch wegen einer falschen URL.
Den Bösewicht, der das Problem verursacht hat, habe ich schnell gefunden. Das Portal hat mehrere AAM’s (Alternate Access Mappings) zum Zugriff auf die SharePoint-Seite zu Verfügung gestellt. Eine URL für den internen Zugriff und eine URL für den öffentlichen Zugriff.

Auf dem Portal gibt es eine „Über“-SiteCollection, die den Einstiegspunkt auf die Website markiert. Diese SiteCollection enthält Listen mit allgemeinen Daten die auch in Sub-SiteCollection verfügbar sein müssen. Dahinter können beliebige Teamsites liegen. Alle Teamsites brauchen Infos aus der „Über“-SiteCollection. Damit dieser Zugriff einfacher wird habe ich in dem Projekte einen Wrapper gebaut, der einen Context zu der „Über“-SiteCollection aufbaut und diese öffnet. Und wie schon oben erwähnt natürlich über IDs.

Portalstruktur visualisiert

Portalstruktur visualisiert


public class UeberContext : IDisposable
{
	private readonly SPContext _spContext;
	private SPSite _uSite;
	private SPWeb _uWeb;

	private UeberContext(SPContext spContext)
	{
		_spContext = spContext;
	}

	private static UeberContext Instance
	{
		get
		{
			var ueberContext = HttpContext.Current.Items["UeberSiteInstance"] as UeberContext;
			if (ueberContext == null)
			{
				var spContext = SPContext.Current;
				var instance = new UeberContext(spContext);
				HttpContext.Current.Items["UeberSiteInstance"] = instance;
				return instance;
			}
			return ueberContext;
		}
	}

	public static UeberContext Current
	{
		get { return Instance; }
	}

	public SPSite Site
	{
		get
		{
			var site = _spContext.Site;
			var rootWeb = site.RootWeb;
			if (IsMasterSite(rootWeb))
				return site;
			else
			{
				var siteID = new Guid(rootWeb.Properties["UeberSiteID"]);
				_uSite = new SPSite(siteID);
				return _uSite;
			}
		}
	}

	public SPWeb Web
	{
		get
		{
			var site = _spContext.Site;
			var rootWeb = site.RootWeb;
			if (IsMasterSite(rootWeb))
				return rootWeb;
			else
			{
				var webID = new Guid(rootWeb.Properties["UeberWebID"]);
				_uWeb = Site.OpenWeb(webID);
				return _uWeb;
			}
		}
	}

	private bool IsMasterSite(SPWeb supposedWeb)
	{
		// Check if the supposedWeb is already the rootweb of the Über sitecollection
		// simplified it's always true for demo code
		var _isMasterSite = true;
		return _isMasterSite;
	}

	public void Dispose()
	{
		if (_uWeb != null)
			_uWeb.Dispose();
		if (_uSite != null)
			_uSite.Dispose();
	}
}

Wie aus dem Code hervorgeht, speichere ich die IDs zur „Über“-SiteCollection im PropertyBag des RootWebs der aktuell besuchten SiteCollection. Ist die aktuelle SiteCollection schon die „Über“-SiteCollection, gebe ich das SPWeb-Objekt und das SPSite-Objekt des SharePoint-Context’s zurück. Wenn nicht wird die „Über“-SiteCollection geöffnet.

Nun wieder zurück zu dem Problem mit den multiplen AAM’s…

Da die „Über“-SiteCollection über die ID geöffnet wird, gibt diese immer den Context des Default AAM zurück. Das bedeutet es wird der Default AAM benutzt obwohl die Seite über den Internet AAM besucht wird.

Ein Beispiel:
Ich habe 2 AAM’s in meiner WebApplication konfiguriert .Einen Default und einen für Internet.
    Default = http://intern.dlindemann.de
    Internet = http://extern.dlindemann.de

Besucht wird eine Sub-SiteCollection über den Internet AAM http://extern.dlindemann.de/meine-subsite. Auf dieser Seite befindet sich ein WebPart, der Daten aus einer Liste in der „Über“-SiteCollection unter http://extern.dlindemann.de abrufen soll. Die IDs zur „Über“-SiteCollection stehen im PropertyBag der Sub-SiteCollection und ich kann mit oben beschriebenem UeberContext auf die „Über“-SiteCollection zugreifen (Variable ueberWeb).

var ueberWeb = UeberContext.Current.Web;
SPList list = ueberWeb.GetList("/lists/einfacheListe");
// ... do something with the list

Das funktioniert soweit ganz gut.
Doch nun enthält diese Liste ein URL-Feld zum verlinken von Bildern, die ich gerne mit dem WebPart ausgeben möchte. Egal ob das URL Feld mit einer vollen URL oder einer server-relativen URL befüllt wurde, SharePoint speichert diese Bilder-URLs immer server-relativ ab, wenn sie innerhalb derselben SharePoint-WebApplication liegen. Externe Bilder werden natürlich mit vollem Pfad gespeichert.

Liest man nun ein Bild aus, wird die URL von SharePoint zu einer vollwertigen URL umgewandelt und das im Context des geöffneten SPSite-Objekts.
Mein Bild mit der URL /SiteCollecionImages/mein-bild.png wird zu http://intern.dlindemann.de/SiteCollectionImages/mein-Bild.png obwohl ich die Website über die URL http://extern.dlindemann.de besuche. Denn mein UeberContext der die SiteCollection mittels ID öffnet gibt mir die URL des Default AAM’s zurück.
Die Folge davon ist, dass das Bild nicht geladen werden kann (HTTP Status 502 Bad Gateway).

var imageItem = list.GetItemById(1);
var imageItemField = item.Fields.GetFieldByInternalName("MyImage");
string fieldValue = item[imageItemField.Id].ToString();
var imageFieldValue = new SPFieldUrlValue(fieldValue);
this.Controls.Add(new Image() { ImageUrl = imageFieldValue.Url });

Die Lösung um das Problem zu beheben ist die URL des aktuellen Context (hier http://extern.dlindemann.de/meine-subsite) zu benutzen, was viel URL zerschneiden mit sich bringt. Oder man benutzt die Methode RebaseUriWithAlternateUri um an die URL des richtigen AAM’s heranzukommen statt die ID zu benutzen. Benutzt man die URL um das SPSite-Objekt zu öffnen dauert es ein paar Millisekunden länger. Doch dafür hat Context garantiert die richtige URL. Benutzt man die Methode RebaseUriWithAlternateUri dauert es nochmal etwas länger und man muss auf die Farm zugreifen, deshalb sollte man bei dieser Methode den AAM Zwischenspeichern.

public SPSite Site
{
	get
	{
		var site = _spContext.Site;
		var rootWeb = site.RootWeb;
		if (IsMasterSite(rootWeb))
			return _spContext.Site;
		else
		{
			var ueberSiteUrl = rootWeb.Properties["UeberSiteUrl"].ToString();

			// getting current aam        
			var rootWebUri = new Uri(rootWeb.Url, UriKind.Absolute);
			var webAppUri = new Uri(rootWebUri.GetLeftPart(UriPartial.Authority));
			SPAlternateUrl currentAam = null;
			SPSecurity.RunWithElevatedPrivileges(() =>
			{
				currentAam = SPFarm.Local.AlternateUrlCollections.LookupAlternateUrl(webAppUri);
			});

			// get url for site    
			var ueberSiteUri = SPFarm.Local.AlternateUrlCollections.RebaseUriWithAlternateUri(new Uri(ueberSiteUrl), currentAam.UrlZone);
			_uSite = new SPSite(ueberSiteUri.ToString());

			// open site with correct url    
			return _uSite;
		}
	}
}

Durch diesen Code wird aus der URL http://intern.dlindemann.de die in dem PropertyBag unter UeberSiteUrl gespeichert wurde automatisch http://extern.dlindemann.de wenn die Seite über den Internet AAM besucht wird.

Ich weiß, der Code gewinnt keinen Schönheitswettbewerb 🙂 . Das liegt daran, dass alles stark vereinfacht ist und kein Caching benutzt wird. Ich hoffe ich kann damit trotzdem die Problematik von URLs und verschiedenen AMM’s aufzeigen.

Happy Coding!

Ich hatte letztens eine Anforderung während eines SharePoint-Projekts einen WebPart zu bauen und Dokumente aus einer Liste zu verlinken.
An sich keine schwierige Aufgabe. Danach kam die nächste Anforderung: Die Datei soll sich wenn möglich bitte in den Office Web Apps öffnen.
Momentan wurden die Dateien als Download angeboten.

Ich wusste zuerst nicht wie ich das machen soll. Vor allem in client-seitigem Code. Ich habe ja einfach nur die Dateien verlinkt.
Zum Glück hat mir mein Kollege Helge de Vries einen super Tipp gegeben:

Mach’s wie SharePoint und hänge ?web=1 an die URL!

Und siehe da, es funktioniert einwandfrei.

Der Grund dafür ist, dass die Erweiterung ?web=1 die Anfrage ändert und SharePoint auffordert die Datei im Browser zu öffnen.
Gibt es für die Datei einen Handler, wie z.B. die Office Web Apps für docx-Dateien, wird das Word Dokument in den Office Web Apps geöffnet.
Für Dateien die vom Browser nicht geöffnet werden können, wird weiterhin ein Download angeboten. Genauso verhält sich eine Datei auch, wenn kein Handler vorhanden ist. Beim Klick auf ein Word-Dokument ohne installierte Office Web Apps wird auch ein Download angeboten.

Bei einfachen Dateien, die vom Browser interpretiert werden können sollte man bei dieser Methode aber aufpassen.
txt-Dateien z.B. werden einfach im Browser angezeigt.

Das System funktioniert natürlich auch anders herum. Hat man Dateien die nicht im Browser oder Office Web Apps geöffnet werden sollen kann man mit dem Parameter ?web=0 dagegen vorgehen.

Arbeitet man mit aufwendigen Display Templates, so muss man diese auch manchmal debuggen. Ein einfacher Weg das zu tun ist das Einbauen des JavaScript Befehls debugger in den Display Template Code. Der Befehl macht genau dasselbe wie sein .NET Pendant Debugger.Launch(), nur eben für JavaScript.

Der Befehl funktioniert in Chrome, in Firefox (mit installiertem Firebug) und im Internet Explorer 11.
Die Developer Tools müssen dabei in allen Browsern geöffnet sein

Im Beispiel mit dem Suchergebnis Item:

<html xmlns:mso="urn:schemas-microsoft-com:office:office" xmlns:msdt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882">
<head>
<title>Testelement</title>

<!--[if gte mso 9]><xml>
<mso:CustomDocumentProperties>
<mso:TemplateHidden msdt:dt="string">0</mso:TemplateHidden>
<mso:MasterPageDescription msdt:dt="string">Zeigt die Standardergebniselement-Vorlage an.</mso:MasterPageDescription>
<mso:ContentTypeId msdt:dt="string">0x0101002039C03B61C64EC4A04F5361F385106603</mso:ContentTypeId>
<mso:TargetControlType msdt:dt="string">;#SearchResults;#</mso:TargetControlType>
<mso:HtmlDesignAssociated msdt:dt="string">1</mso:HtmlDesignAssociated>
<mso:ManagedPropertyMapping msdt:dt="string">'Title':'Title','Path':'Path','Description':'Description','EditorOWSUSER':'EditorOWSUSER','LastModifiedTime':'LastModifiedTime','CollapsingStatus':'CollapsingStatus','DocId':'DocId','HitHighlightedSummary':'HitHighlightedSummary','HitHighlightedProperties':'HitHighlightedProperties','FileExtension':'FileExtension','ViewsLifeTime':'ViewsLifeTime','ParentLink':'ParentLink','FileType':'FileType','IsContainer':'IsContainer','SecondaryFileExtension':'SecondaryFileExtension','DisplayAuthor':'DisplayAuthor'</mso:ManagedPropertyMapping>
<mso:HtmlDesignConversionSucceeded msdt:dt="string">True</mso:HtmlDesignConversionSucceeded>
<mso:HtmlDesignStatusAndPreview msdt:dt="string">http://dl-sp15l:40510/sites/s01/_catalogs/masterpage/Display Templates/Custom/Item_Test.html, Konvertierung erfolgreich.</mso:HtmlDesignStatusAndPreview>
</mso:CustomDocumentProperties>
</xml><![endif]-->
</head>
<body>
    <div id="Item_Test">
<!--#_
        if(!$isNull(ctx.CurrentItem) && !$isNull(ctx.ClientControl)){
            debugger;
            var id = ctx.ClientControl.get_nextUniqueId();
            var itemId = id + Srch.U.Ids.item;
			var hoverId = id + Srch.U.Ids.hover;
			var hoverUrl = "~sitecollection/_catalogs/masterpage/Display Templates/Search/Item_Default_HoverPanel.js";
            $setResultItem(itemId, ctx.CurrentItem);
			if(ctx.CurrentItem.IsContainer){
				ctx.CurrentItem.csr_Icon = Srch.U.getFolderIconUrl();
			}
			ctx.currentItem_ShowHoverPanelCallback = Srch.U.getShowHoverPanelCallback(itemId, hoverId, hoverUrl);
            ctx.currentItem_HideHoverPanelCallback = Srch.U.getHideHoverPanelCallback();
_#-->
            <div id="_#= $htmlEncode(itemId) =#_" name="Item" data-displaytemplate="DefaultItem" class="ms-srch-item" onmouseover="_#= ctx.currentItem_ShowHoverPanelCallback =#_" onmouseout="_#= ctx.currentItem_HideHoverPanelCallback =#_">
				_#=ctx.RenderBody(ctx)=#_
                <div id="_#= $htmlEncode(hoverId) =#_" class="ms-srch-hover-outerContainer"></div>
            </div>
<!--#_
        }
_#-->
    </div>
</body>
</html>
Debugger startet im JavaScript

Debugger startet im JavaScript