Daniel's Blog

.NET and SharePoint Development
Google+
View my Google+ profile
XING
View my XING profile

Ribbon Commands wiederverwenden

Möchte man SharePoint Ribbon Commands wiederverwenden, um z.B. einen Button in einem PageLayout mit einer Funktion des Ribbons auszustatten, kann man das sehr einfach über eine Zeile Javascript-Code bereitstellen:

SP.Ribbon.PageManager.get_instance().get_commandDispatcher().executeCommand(<CommandName>, <OptionalParameters>);

 

Die Commands kann man sehr gut aus der CMDUI.XML lesen.

Hier ein Beispiel für den Ordner erstellen Dialog:

SP.Ribbon.PageManager.get_instance().get_commandDispatcher().executeCommand("NewFolder",null);

Kick it on dotnet-kicks.de

Einfaches Handling von SharePoint Ribbon Postback Commands

In der Welt der SharePoint Ribbon Erweiterungen muss man zwingend auf JavaScript zurückgreifen um Aktionen auszuführen.
Dafür gibt es ja das neue JavaScript Client Object Model. Doch dieses Framework stößt auch irgendwann an seine Grenzen.
So kann es z.B. bei lange andauernden, complexen Abfragen vorkommen, dass der Browser einfriert. Trotz Asynchronität.
Außerdem sollte man bei solchen Queries bedenkten, dass man viele Zeilen JavaScript Code schreiben muss, der viele Delegates enthalten wird. Dies führt zu einer schlechteren Lesbarkeit des Codes und bläht diesen nur unnötig auf.
Viel einfacher können solche Methoden serverseitig entwickelt werden. Und zwar durch den Einsatz eines Delegate-Controls in Verbindung mit einem IPostBackEventHandler.

Erweiterte PageComponente

Aber zuerst einmal alles zurück auf Anfang. Zunächst benötigt man eine PageComponente.
Ein PageComponent-Objekt ermöglicht das Interagieren mit dem Ribbon (http://msdn.microsoft.com/en-us/library/ff407303.aspx).

Type.registerNamespace('DL.PostbackRibbon');
// RibbonApp Page Component
DL.PostbackRibbon.PageComponent = function () {
	DL.PostbackRibbon.PageComponent.initializeBase(this);
}
DL.PostbackRibbon.PageComponent.initialize = function () {
	ExecuteOrDelayUntilScriptLoaded(
		Function.createDelegate(
			null,
			DL.PostbackRibbon.PageComponent.initializePageComponent),
		'SP.Ribbon.js');
}
DL.PostbackRibbon.PageComponent.initialize = function (controlId) {
	DL.PostbackRibbon.PageComponent.ControlClientId = controlId;
	ExecuteOrDelayUntilScriptLoaded(
		Function.createDelegate(
			null,
			DL.PostbackRibbon.PageComponent.initializePageComponent),
		'SP.Ribbon.js');
}
DL.PostbackRibbon.PageComponent.initializePageComponent = function () {
	var ribbonPageManager = SP.Ribbon.PageManager.get_instance();
	if (null !== ribbonPageManager) {
		ribbonPageManager.addPageComponent(DL.PostbackRibbon.PageComponent.instance);
		ribbonPageManager
			.get_focusManager()
			.requestFocusForComponent(DL.PostbackRibbon.PageComponent.instance);
	}
}
DL.PostbackRibbon.PageComponent.refreshRibbonStatus = function () {
	SP.Ribbon.PageManager
		.get_instance()
		.get_commandDispatcher()
		.executeCommand(Commands.CommandIds.ApplicationStateChanged, null);
}
DL.PostbackRibbon.PageComponent.ControlClientId = null;

DL.PostbackRibbon.PageComponent.prototype = {
	init: function () {
		// if you have something to initalize
	},
	getFocusedCommands: function () {
		return [];
	},
	getGlobalCommands: function () {
		// Server side commands will show up here
		return getGlobalCommands();
	},
	isFocusable: function () {
		return true;
	},
	canHandleCommand: function (commandId) {
		return commandEnabled(commandId);
	},
	handleCommand: function (commandId, properties, sequence) {
		return handleCommand(commandId, properties, sequence);
	}
}

// Register classes
DL.PostbackRibbon.PageComponent.registerClass('DL.PostbackRibbon.PageComponent', CUI.Page.PageComponent);
DL.PostbackRibbon.PageComponent.instance = new DL.PostbackRibbon.PageComponent();

// Notify and execute jobs
NotifyScriptLoadedAndExecuteWaitingJobs("/_layouts/ListinfoRibbonPostbackCommand/DL.PostbackRibbon.PageComponent.js");

Hier gibt es auch die erste Anpassung die ich gemacht habe.
In Zeile 36 habe ich die Variable ControlClientId erstellt, die später die ID des IPostBackEventHandler hält. Somit wird sichergestellt, dass der richtige Handler benutzt wird.
Zeile 13 und 14 zeigen die angepasste Initialize-Methode, die die Control-ID setzt.
Ansonsten ist es eine einfache PageComponente wie in der MSDN beschrieben.

Das CommandHandler Control

Jetzt die spannende Serverseite. Ich habe ein Cotrol erstellt, das das Interface IPostBackEventHandler implementiert.

public class PostbackRibbonHandler : Control, IPostBackEventHandler
{
	public PostbackRibbonHandler()
	{
		ID = "PostbackRibbonHandler";
	}

	protected override void CreateChildControls()
	{
		base.CreateChildControls();

		// register server side command
		List<IRibbonCommand> cmds = new List<IRibbonCommand>()
		{
			new SPRibbonPostBackCommand("RunPostback", this, "RunPostback", null)
		};

		SPRibbonScriptManager sm = new SPRibbonScriptManager();

		// register the page component
		sm.RegisterInitializeFunction(this.Page,
			"InitPageComponent",
			"/_layouts/ListinfoRibbonPostbackCommand/DL.PostbackRibbon.PageComponent.js",
			false,
			"DL.PostbackRibbon.PageComponent.initialize('" + this.ClientID + "')");

		// enable server registered commands on the page
		sm.RegisterGetCommandsFunction(this.Page, "getGlobalCommands", cmds);
		sm.RegisterCommandEnabledFunction(this.Page, "commandEnabled", cmds);
		sm.RegisterHandleCommandFunction(this.Page, "handleCommand", cmds);
	}

	public void RaisePostBackEvent(string eventArgument)
	{
		// implement PostBack logic
		// by default eventArgument contains the name of the command
	}
}

Im Konstruktor gebe ich dem Control einen Namen, damit man später einen PostBack auf dem Control abfeuern kann. Hier werden auch die Commands sowie die PageComponenten registriert die später von dem Control behandelt werden sollen.
Zeile 25 enthält den wichtigsten Schritt: Das setzen der Control-ID in die PageComponente.

PostBack ausführen

Nun wieder zurück zur PageComponente. In der Methode handleCommand wird der PostBack ausgelöst, der den IPostBackEventHandler triggert.
Dabei wird als Argument ein kleines JSON-Objekt übergeben, das mir die benötigten SharePoint Listen- und Iteminformationen bereitstellt.

handleCommand: function (commandId, properties, sequence) {
	if (commandId === 'RunPostback') {
		if (!CUI.ScriptUtility.isNullOrUndefined(DL.PostbackRibbon.PageComponent.ControlClientId)) {
			var controlId = DL.PostbackRibbon.PageComponent.ControlClientId;
			__doPostBack(controlId.replace('_', '$'), '{"id":"RunPostback","selectedId":' + SP.ListOperation.Selection.getSelectedItems()[0].id + ', "listId":"' + GetCurrentCtx().listName + '"}');
			return true;
		}
		else {
			if (window.console)
				window.console.error('Unable to do a postback. PostBackHandler not found.')
		}
	}
	else {
		return handleCommand(commandId, properties, sequence);
	}
}

Verarbeiten der Daten

Die PostBack Argumente kann man sehr schnell über den JavaScriptSerializer zu einem Objekt umwandeln.
Danach führt man wie gewohnt SharePoint Operationen aus.

In diesem Beispiel setze ich einen neuen Titel für das ausgewählte Dokument.

public void RaisePostBackEvent(string eventArgument)
{
	JavaScriptSerializer jss = new JavaScriptSerializer();
	var args = jss.Deserialize<SPRibbonCommandArgs>(eventArgument);

	switch (args.Id)
	{
		case "RunPostback":
			SPWeb web = SPContext.Current.Web;
			SPList list = web.Lists[args.ListId];
			if (list != null)
			{
				SPListItem item = list.GetItemById(args.SelectedId);
				if (item != null)
				{
					SPField titleField = item.Fields.GetFieldByInternalName("Title");
					item[titleField.Id] = "Mein neuer Titel";
					item.SystemUpdate();
				}
			}
			break;
	}
}
[Serializable]
public class SPRibbonCommandArgs
{
	/// <summary>
	/// Command ID
	/// </summary>
	public string Id { get; set; }

	/// <summary>
	/// ID of the Selected Item
	/// </summary>
	public int SelectedId { get; set; }

	/// <summary>
	/// ID of the List
	/// </summary>
	public Guid ListId { get; set; }
}

Ribbon Erweiterung und Delegate-Control

Dann fehlt ja “nur” noch die Ribbon definition. Diese besteht in meinem Fall aus einem einzelnen Button, der in jeder Dokumentenbibliothek eingeblendet wird.
Klickt man auf den Button wird das Klick-Event an die PageComponente geliefert und diese triggert per PostBack den IPostBackEventHandler.

In diese Datei kann man auch direkt das Delegate-Control registieren, wie in Zeile 34.

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
	<CustomAction Id="DL.PostbackCommand" Location="CommandUI.Ribbon" RegistrationId="101" RegistrationType="List">
		<CommandUIExtension>
			<CommandUIDefinitions>
				<CommandUIDefinition Location="Ribbon.Documents.Copies" />
				<CommandUIDefinition Location="Ribbon.Documents.Workflow" />
				<CommandUIDefinition Location="Ribbon.Documents.Groups._children">
					<Group Id="Ribbon.Documents.CustomGroup"
						   Sequence="55"
						   Description="Custom Group"
						   Title="Custom"
						   Template="Ribbon.Templates.Flexible2">
						<Controls Id="Ribbon.Documents.CustomGroup.Controls">
							<Button Id="Ribbon.Documents.CustomGroup.RunPostback"
									Command="RunPostback"
									Image16by16="/_layouts/images/ListinfoRibbonPostbackCommand/runit_32x32.png"
									Image32by32="/_layouts/images/ListinfoRibbonPostbackCommand/runit_32x32.png"
									LabelText="Postback!"
									TemplateAlias="o2"
									Sequence="15" />
						</Controls>
					</Group>
				</CommandUIDefinition>
				<CommandUIDefinition Location="Ribbon.Documents.Scaling._children">
					<MaxSize Id="Ribbon.Documents.Scaling.CustomGroup.MaxSize"
							 Sequence="15"
							 GroupId="Ribbon.Documents.CustomGroup"
							 Size="LargeLarge" />
				</CommandUIDefinition>
			</CommandUIDefinitions>
		</CommandUIExtension>
	</CustomAction>
	<Control Id="AdditionalPageHead" ControlAssembly="ListinfoRibbonPostbackCommand, Version=1.0.0.0, Culture=neutral, PublicKeyToken=098b81014c222ac1" ControlClass="ListinfoRibbonPostbackCommand.Controls.PostbackRibbonHandler"></Control>
</Elements>

That’s it.
Da diese ganze Mischung aus JavaScript, C# und XML auf den ersten Blick wohl etwas unübersichtlich ist gibt es hier noch das fertige Projekt zum Download.

  ListinfoRibbonPostbackCommand.zip

Kick it on dotnet-kicks.de

ListView Item reselektieren

Das ModalDialog-Framework in SharePoint ist eine nette Sache. Leider hat es das Problem, dass ausgewählte Listenelemente nach dem Aktualisieren der Seite wieder deselektiert sind.

Möchte man über den Ribbon bzw. mehrere Dialoge einen “Workflow” abbilden ist das Verhalten sehr störend (z.B. wenn ein Benutzer mehrere Dialog öffnen muss um einen bestimmten Status auf ein Element zu setzen).

Die gute Nachricht: Durch das Neuladen der Seite mittels SP.UI.ModalDialog.RefreshPage wird ein POST ausgelöst.
Somit kann man sich die Daten in einem HiddenField speichern. Einfachste Lösung wäre hier ein Delegate-Control, das die ID des selektierten Elemtents wieder ausliest.

Doch wie selektiert man das Element wieder?

Microsoft hat bei der Entwicklung des Userinterfaces schon an alles gedacht. Ich musste allerdings lange suchen, bis ich die richtige Funktion gefunden habe:

ToggleItemRowSelection2(ctxCur, tr, fSelect, fUpdateRibbon)

Die Funktion stammt aus der SharePoint core.js.

Die Parameter:

  • ctxCur: Der aktuelle SharePoint Context der Seite
  • tr: Das DOM-Objekt der auszuwählenden TableRow
  • fSelect: Ein bool-Wert, der angibt ob das Element selektiert oder deselektiert wird
  • fUpdateRibbon: bool, Gibt an ob die Command-UI (Ribbon) aktualisiert werden soll

Hier ist ein Ausschnitt aus meiner Lösung. Voraussetzung ist ein HiddenField, das per Klick-Event die ID der selektierten Zeile speichert.

$(function () {
	// add click event to row
	$('tr.ms-itmhover').click(function () {
		var iidAttributeValue = $(this).attr('iid');
		var iid = iidAttributeValue.split(',');
		if (iid.length == 3) {
			$('#thehiddenfieldid').val(iid[1]);	// set selection to hidden field
		}
	});

	// read value from hidden field and reselect the row
	var currentCtx = GetCurrentCtx();	// method from CUI.js
	var itemId = $('#thehiddenfieldid').val();		// get id from hiddenfield
	var jTr = $('tr[iid*=",' + itemId + ',"]:first');	// tr to select

	if (!CUI.ScriptUtility.isNullOrUndefined(currentCtx) &amp;&amp; jTr.length &gt; 0) {
		ToggleItemRowSelection2(currentCtx, jTr[0], true, true);
	}
});

 

Kick it on dotnet-kicks.de

MySites Konfiguration “debuggen”

Die MySites in SharePoint 2010 sind sehr auf den Austausch von persönlichen Informationen ausgerichtet. Neu hinzu kommt eine Pinnwand à la Facebook und die Möglichkeit Statusinformationen bereitzustellen, wie auf XING.
Das Aufsetzen von MySites kann jedoch etwas knifflig sein. Ein bekanntes Problem ist z.B. dass der Serviceaccount, der den User Profile Synchronization Services ausführt zwingend zur lokalen Administratorengruppe hinzugefügt werden muss (weitere Informationen dazu gibts auf Fabians Blog).
Deshalb möchte ich hier kurz zusammenfassen auf was man achten muss um MySites zu erstellen und zeigen wie man Fehlerquellen beim synchronisieren auf die Schliche kommt.

Die Konfiguration

  • Farm/Service Account muss zur Gruppe Administratoren gehören
    Der Benutzer der den User Profile Synchronisation Service ausführt musst zur lokalen Administratorgruppe gehören.
  • Zugriffsrechte auf die Sharepoint Datenbank
    Der Benutzer der den User Profile Synchronisation Service ausführt muss Daten in die Datenbank schreiben können.
  • Prüfen, dass die Windows Services Forefront Indentity Snychronisation und Forefront Identity Manager laufen
    Nachdem man den User Profile Synchronisation Service gestartet hat (der Start kann bis zu 10min dauern), sollte man sich vergewisser, dass die Windows Services Forefront Indentity Snychronisation Service und Forefront Identity Manager Service gestartet wurden. Diese werden von dem User Profile Synchronisation Service ausgeführt.
  • IIS Reset
    Nach dem Starten des Services sollte man einen IIS Reset durchführen, da man sonst eine System.IO.FileLoadException an den Kopf geworfen bekommt :/
  • Benötigte Rechte und Daten des Active Directories zu synchronisieren
    Der Benutzer der Daten aus dem Active Directory zieht, braucht das Recht Replicate Directory Changes. Meistens ist das der SharePoint Farm Account, es kann aber auch ein anderer Account angegeben werden.

Weitere Informationen zu diesen Themen findet man auch im Microsoft Technet: http://technet.microsoft.com/en-us/library/ee721049(office.14).aspx

Probleme mit dem Tool Forefront Synchronisation Service Manager lösen

Der Forefront Synchronisation Service Manager ist ein nettes Tool mit dem man die Konfiguration des User Profile Synchronisation Services überprüfen kann.
Man findet das Programm unter C:\Program Files\Microsoft Office Servers\14.0\Synchronization Service\UI\Shell\miisclient.exe.
Mit diesem Tool hat man die Möglichkeit sich eine Liste der Operationen auf ein Active Directory oder auf eine andere Datenquellen anzuschauen, kann sich Operationen (wie z.B. einen FullImport) starten und sich eine genaue Fehlermeldung anzeigen lassen.

Forefront Identity Manager Client

Um das Programm starten zu können muss der Benutzer in folgenden Gruppen sein:

  • FIMSyncBrowse
  • FIMSyncJoiners
  • FIMSyncOperators
  • FIMSyncPasswordSet

Danach startet man das Programm im Context des Service Accounts.

Nun sieht man eine Liste der ausgeführten Aktionen.

Synchronisation Service Manager Übersicht

Um z.B. einen FullImport laufen zu lassen, kann man über Actions -> Run (bzw. STRG+F5) eine Aktion starten.

Synchronisation Service Manager Action

Möchte man wissen, warum eine Synchronisation nicht geklappt hat, kann man sich einfach das Log anschauen.

Synchronisation Service Manager Connection Log

Der Synchronisation Service Manager hat noch einiges mehr zu bieten. Für ein kleines Debugging sollten die beschriebenen Funktionen jedoch ausreichen.

Kick it on dotnet-kicks.de

Entwickeln mit Hilfe des Developer Dashboards

Eines der wichtigsten Features für Entwickler im neuen SharePoint 2010 ist das
Developer Dashboard.
Wie man das Developer Dashboard aktiviert habe ich schon hier beschrieben.
Doch wie benutzt man das Feature um seinen Code zu analysieren?

Zeit stoppen

In der linken Spalte des Developer Dashboards findet man die Ablaufverfolgung des Requests.
Hier findet man eine Liste von ausgeführten Aktionen mit Angaben zur Dauer der Verarbeitung.
Um eigene Aktionen aufzunehmen benutzt man das Objekt SPMonitoredScope, das sich im Namespace Microsoft.SharePoint.Utilities befindet.
Jeglicher Code, der ausgeführt wird solange eine Instanz von SPMonitoredScope vorhanden ist wird protokolliert.
Deshalb sollte man SPMonitoredScope über usings steuern.

protected void myButton_Click(object sender, EventArgs e)
{
    using (SPMonitoredScope scopeButtonPushed = new SPMonitoredScope("Button action"))
    {
        // dummy sleep action
        Thread.Sleep(5000);
        l.Text = "Meine Daten: ";

        string myData = GenerateLabelData();
        l.Text += myData;
    }
}

Dieser Beispielcode nimmt auf, dass die Aktion “Button action” 5 Sekunden gedauert hat und zeigt die Daten in der Ablaufverfolgung an.

Es besteht auch die Möglichkeit Aktionen zu verschachteln.
Somit kann man eine detaillierte Informationen zu seinem Code bekommen.

protected void myButton_Click(object sender, EventArgs e)
{
    using (SPMonitoredScope scopeButtonPushed = new SPMonitoredScope("Button action"))
    {
        // dummy sleep action
        Thread.Sleep(5000);
        l.Text = "Meine Daten: ";

        string myData = GenerateLabelData();
        l.Text += myData;
    }
}

private string GenerateLabelData()
{
    // z.B. lesen von daten aus einer datenbank
    using (SPMonitoredScope scopeGetData = new SPMonitoredScope("Get user information"))
    {
        // ... und noch ein dummy sleep
        Thread.Sleep(3000);
        return "Daniel Lindemann";
    }
}

In diesem Beispiel wird die Aktion “Get user information” innerhalb der Aktion “Button action” ausgeführt.
Dies erzeugt folgende Ausgabe im Developer Dashboard:

Informationen im Developer Dashboard

Informationen im Developer Dashboard

Wichtige Ereignisse protokollieren

Auf der rechten Seite des Developer Dashboards findet man die Spalte Assertionen und kritische Ereignisse bzw. Asserts and Critical Events in der englischen Version.
Wie der Name schon verrät können hier kritische Ereignisse Überprüft werden.
Man sollte diese Funktion benutzten um darauf hinzuweisen, dass etwas nicht wie gewünscht ausgeführt wurde. Dazu benutzt man die statische Methode AddDataToScope der Klasse SPCriticalTraceCounter die sich ebenfalls im Namespace Microsoft.SharePoint.Utilities befindet.
Als Beispiel ändere ich die Methode GenerateLabelData() sodass ein Eintrag zu den Ereignissen hinzugefügt wird:

private string GenerateLabelData()
{
    // z.B. lesen von daten aus einer datenbank
    using (SPMonitoredScope scopeGetData = new SPMonitoredScope("Get user information"))
    {
        // ... und noch ein dummy sleep
        Thread.Sleep(3000);
        SPCriticalTraceCounter.AddDataToScope(2501, "Database problem", 1, "Database query took long time");
        return "Daniel Lindemann";
    }
}

Als 3. Parameter erhält die Methode ein Flag, das beschreibt um welche Art von Meldung es sich handelt.
Folgende Meldungen sind möglich:

  • 1 = Critical
  • 4 = Exception
  • 6 = Assert
  • 8 = Warning
  • 10 = Unexpected
  • 15 = Monitorable

Diese Methode um den Ablauf des Codes zu verfolgen hat jedoch einen Nachteil. Es wird immer der komplette StackTrace mitgespeichert und somit kann die Antwort des Servers um einige kB anwachsen.
Darum sollte die Methode nicht für jedes Problem das auftreten kann aufgerufen werden.

Kick it on dotnet-kicks.de