Daniel's Blog

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

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