Newsfeed Posting per Code

Newsfeed Postings können in SharePoint auch mittels Client Object Model durchgeführt werden. Dafür muss die dll “Microsoft.SharePoint.Client.UserProfiles” aus dem ISAPI Verzeichnis referenziert werden. Das Posting selbst muss mit einem ClientContext für den SP User durchgeführt werden. Ein Versuch mit einem App ClientContext schlägt fehl. Da in meinem BSP mit Office365 gearbeitet wird, müssen Credentials für SharePoint Online verwendet werden. (zeile 8 bis 11) Ein Post wird mit der Methode CreatePost (Zeile 19) einer Instanz des SocialFeedManagers (Zeile 13) erstellt. Der erste Parameter ist die URL des Newsfeeds oder kann auch NULL sein. Im Falle von NULL wird das Posting im Newsfeed des User in seiner MySite erstellt. Der gesamte Code: 1: var spContext = SharePointContextProvider.Current.GetSharePointContext(Context); 2:   3: using (var clientContext = new ClientContext(spContext.SPHostUrl)) 4: { 5: string pwd = PASSWORD; 6: string userEmail = USERLOGIN; 7:   8: SecureString str = new SecureString(); 9: pwd.ToList().ForEach(c => str.AppendChar(c)); 10:   11: clientContext.Credentials = new SharePointOnlineCredentials(userEmail, str); 12:   13: SocialFeedManager socMan = new SocialFeedManager(clientContext); 14: SocialPostCreationData dat = new SocialPostCreationData 15: { 16: ContentText = TextBox1.Text + " " + DateTime.Now.ToShortTimeString() 17: }; 18:   19: socMan.CreatePost(spContext.SPHostUrl.ToString(), dat); 20: clientContext.ExecuteQuery(); 21:   22: TextBox1.Text = ""; 23: } .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }

App Part - Parameter

Serverseitige Webparts in SharePoint können konfiguriert werden. Gleiches möchte man euch bei App Parts erreichen. Jedoch werden SharePoint App Parts IFRAME in die Webseite eingebettet. Server Seitigen Code gibt es hier nicht. Allerdings können in der Elements-Datei des App Parts Properties definiert werden. Im ClientWebPart Knoten der XML Datei ist folgender Eintrag möglich: <Properties>       <Property Name="Konfiguration1"                 Type="string"                 WebBrowsable="true"                 WebDescription="Test Konfiguration"                 WebDisplayName ="Konfig1"                 RequiresDesignerPermission="true"                 DefaultValue="">       </Property>     </Properties>   Dieser Eintrag ermöglicht das Konfigurieren. Allerdings muss der Konfigurationswert auch an der App part übergeben werden. Und dies wird in den Aufruf-Parameter der URL erledigt, jedoch muss dies ebenfalls konfiguriert werden. Für jedes Property wird ein Platzhalter mit _ vor und nach dem Namen angelegt und dieser kann in der URL verwendet werden. Die URL ist ebenfalls in der Elements Datei beim Content Type anzugeben: <Content Type="html" Src="~remoteAppUrl/Pages/DemoAppPart.aspx?{StandardTokens}&amp;Konfiguration1=_konfiguration1_&amp;EditMode=_editMode_" />

Sharepoint Foundation 2013 – Secure Store


Andreas Rauch

Ganz ehrlich.. es gibt ein paar Dinge, die einen ziemlich ärgern. Dazu gehört der Secure Store des Sharepoint Foundation 2013. Auf einen Nenner gebracht. Den gibst da nicht. Warum mich das so ärgert ist die Tatsache, dass er scheinbar dabei ist. Er lässt sich als Dienstanwendung anlegen, quittiert aber seinen Dienst mit “Testzeitraum abgelaufen..”   Secure Store? Der Secure Store (Single Sign On) hat sein Einsatzgebiet im Weiterreichen von Anmeldungen. So zum Beispiel, möchte man im SQL Server nicht jedem einzelnen Benutzer Logins verpassen, sondern ein Sammelkonto. Oder evtl. lässt sich technisch gar nicht der Zugriff per Windows Konto einrichten, sondern muss per SQL Authentifizierung geschehen. Typische Szenarien für den Secure Store. Ohne den SecureStore wird es schon schwieriger… Nehmen wir mal den Fall an, wir möchten mittels Business Data Connectivity Service (BDC) auf SQL Tabellen zugreifen, dann verlangt der BDC entweder ein SecureStore Konto oder das Weiterreichen des aktuellen Windows Benutzers. Tja, Pech gehabt in der Foundation. Denn nur Fall 2 – das Weiterreichen des aktuellen Windows Konto -  funktioniert ohne Secure Store, aber auch nur, wenn man mit Kerberos Authentifizierung arbeitet. (Dahinter steckt das sogenannte double hop Problem). Es gibt zwar Workarounds wie Reverttoself, so dass der Applicationpool Account auf den SQL Server zugreift, aber empfohlen ist die Maßnahme nicht. http://blogs.msdn.com/b/bcs/archive/2010/03/12/authenticating-to-your-external-system.aspx   Sharepoint Foundation SecureStore Egal was man versucht dran zu drehen. Es klappt nicht. Und wird nie klappen.. In zahlreichen Foren wurde das Problem besprochen. Lösungen für SPF 2013 gab's nie. Letztendlich wurde bekannt, dass der Secure Store nur in der Enterprise Version zu haben ist. Das gewünscht Update bzw Hotfix, das zumindest den scheinbar möglichen  SecureStore entfernt, gibt's bis dato noch nicht. (Jan 2014) Was mich umso mehr ärgert, ist, dass bereits Sharepoint Foundation 2010 mit der gleichen Meldung aufwartete. Nur dort war  das Problem aber zu beheben.   Hier ein paar Quellen zum Thema: http://ezybzy.info/node/309 https://social.technet.microsoft.com/Forums/office/en-US/707504d4-bd13-400a-8240-a35ce0258d79/the-trial-period-for-this-product-has-expired-or-secure-store-shared-service-is-not-supported-for?forum=sharepointadmin   Alternativen? Für den SecureStore nicht wirklich! Vr allem was das Zusammenspiel mit Externen Inhaltstypen des BDC betrifft. Eine Datenquellenansicht zusammenzubauen ist im Sharepoint Designer 2013 gut zu bewerkstelligen. dazu in einem späteren Artikel.

Timer Jobs für Office365–SharePoint WebSites

Timer Jobs werden in SharePoint onPremise regelmäßig für wiederkehrenden Aktivitäten eingesetzt. Ein TimerJob muss als Farm-Solution programmiert werden. Und hier stößt man bei Office365 an Grenzen. Eine Farmsolution ist nicht möglich. Um TimerJobs aber auch unter Office365 zu nutzen, muss auf die Webaufträge einer Azure-WebSite ausgewichen werden. Ein Webauftrag ist nichts anderes als eine Konsolen -Anwendung die über das Azure-Portal ausgeführt wird. Dies kann auch zeitgesteuert erfolgen. Wenn nun diese Konsolen -Anwendung auf SharePoint zugreifen kann, ist der Timer Job auch schon wieder fertig! Die Konsolen Anwendung als SharePoint App Jede Anwendung kann als SharePoint App agieren, wenn sie in SharePoint registriert wurde. Damit wird ein Zugriff auf SharePoint ermöglicht. Um eine App zu registrieren, ist es notwendig einen App-Principal zu erstellen. Dieser ist der App-SharePoint User dem Berechtigungen erteilt werden. Im Layouts Verzeichnis kann die Seite “_layouts/AppRegNew.aspx” aufgerufen werden um die App zu registrieren.   Mit den beiden “Generieren”-Schaltflächen können zufällige Werte für Client-ID und “Geheimer Clientschlüssel” erstellt werden. Den Clientschlüssel habe ich für den Screenshot abgeändert Als App-Domäne verwendet ich “localhost”. Nach dem Click auf “Erstellen” erhält man eine Zusammenfassung der erstellten App. Es ist ratsam, den Inhalt in die Zwischenablage zu kopieren, und von dort in den Code der Anwendung in ein Kommentar schreiben. Die App ist zwar nun registriert, allerdings hat der App-User noch keine Rechte. Daher ist es notwendig die Permissions zu setzen. In einem App-Projekt geschieht dies in der Manifest-Datei. Bei unserem Timerjob setzen wir die App-Permission-Requests über die Weboberfläche. Unter _layouts/AppInv.aspx können wir eine registrierte App aufrufen. Allerdings benötigen wir die App Id. Kein Problem, wir haben sie ja vorher in die Zwischenablage kopiert. Allerdings sprach die Registrierungsseite von einer “Client-ID” (Ob hier zwei unterschiedliche Entwickler daran gearbeitet haben ) Mit einem Klick auf “Nachschlagen” kann die App aufgerufen werden und die “Berechtigungsanforderungs-XML” gesetzt werden. Der XML Code entspricht dem, der auch bei einem App-Projekt erstellt wird. Um das Erstellen zu vereinfachen kann man auch einfach ein App-Projekt beginnen. Danach die Permissions mit dem Editor setzen und so den XML Code kopieren. Zu beachten ist, dass die App danach mit App-Ony Rechten arbeiten soll. (Siehe meinen Blogzu App-Only-Calls) In meinem Beispiel setze ich weitreichende Rechte: 1: <AppPermissionRequests AllowAppOnlyPolicy="true"> 2: <AppPermissionRequest Scope="http://sharepoint/content/tenant" Right="Manage" /> 3: </AppPermissionRequests> .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } Nach dem Klick auf “Erstellen” kommt die Vertrauens-Frage: Jeder darf für sich entscheiden ob er seinem Code vertraut Der Programmcode der Anwendung Nachdem nun die App registriert wurde, muss die Anwendung selbst erstellt werden. Wir starten mit einem simplem Konsolen-Projekt. Da wir aber mit AccessToken udgl hantieren müssen, ist es ratsam über NuGet das Package “AppForSharePointWebToolkit” zu laden. Dieses bringt die Klassen für SharePointContext und den TokenHelper in unser Projekt. Außerdem werden die Dlls-für SharePoint CSOM registriert. Das ClientSecret und der ClientID müssen in der App.config Datei der Anwendung hinterlegt werden. Die App-Config sollte dann in etwa so aussehen (ClientSecret habe ich wieder verändert ). <configuration> <appSettings> <add key="ClientId" value="c33f72da-0344-4c02-9028-dc720bba11e0"/> <add key="ClientSecret" value="MRuC...........="/> </appSettings> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" /> </startup> </configuration> .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } Im Wesentlichen enthält die Main-Funktion nur Aufrufe ins CSOM. Der dafür notwendige ClientContext wird über die Methode “GetClientContextWithAccessToken” der Klasse “Tokenhelper” erstellt. 1: static void Main(string[] args) 2: { 3: string url = "https://......./sites/Dev"; 4: Uri uri = new Uri(url); 5: string realm = TokenHelper.GetRealmFromTargetUrl(uri); 6: string accessToken = TokenHelper.GetAppOnlyAccessToken( 7: TokenHelper.SharePointPrincipal, 8: uri.Authority, realm).AccessToken; 9:   10: using (var ctx = TokenHelper.GetClientContextWithAccessToken(url, accessToken)) 11: { 12: List liste = ctx.Web.Lists.GetByTitle("TestListe"); 13: var neuesItem = liste.AddItem(new ListItemCreationInformation()); 14: neuesItem["Title"] = string.Format("Last Run at {0}", DateTime.Now.ToShortTimeString()); 15: neuesItem.Update(); 16: ctx.ExecuteQuery(); 17: } 18: }.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } Nachdem die Anwendung fertig ist, muss diese nun noch auf Azure deployed werden. Job-Einrichtung unter Azure Ein Webauftrag unter Azure wird als .ZIP-Datei in Azure hochgeladen. In der Zip-Datei muss ein EXE liegen. Also kann unsere Konsolen-Anwendung kompiliert werden, und das Debug- oder Release-Verzeichnis in eine ZIP Datei gepackt werden. In der Verwaltungsseite einer Azure-Website ist die Seite “Webjobs” zu finden. Hiermit kann ein neuer Job angelegt werden. Auf Seite 1 muss ein Name für den Job angegeben werden. Weiters muss die Zip Datei ausgewählt werden und es muss definiert werden, ob der Job zeitgesteuert (Run on a schedule) gestartet wird. Im Fall einer Zeitsteuerung muss auch die Region ausgewählt werden. Auf Seite 2 wird noch das Wiederholungsintervall sowie Start- und End-Zeitpunkt definiert: Danach wird die ZIP Datei hochgeladen und der TimerJob ist fertig konfiguriert!

Custom Code Activity für SharePoint 2013 Workflow

Der Workflow Manager bringt schon einige Activities mit. Jedoch ist es manchmal wünschenswert eigene Activities mit C# Code zu erstellen und nicht alles nur im Designer “zusammen zu klicken” Im großen Überblick sind die notwendigen Steps einfach. 1) Die Activity ist eine von “CodeActivity” abgeleitete Klassen. Die Methode “Execute” wird vom Workflow aufgerufen. 2) das Assembly mit der Klasse muss auf der Maschine mit dem Workflowmanager deployed werden. So schnell der Teil 1 erledigt ist, so mühsam ist Teil 2, weil hierfür kein Template bereitgestellt wird. Zunächst legen wir ein neues Visual Studio Projekt an. Als Vorlage wird “Activity Library” ausgewählt. Dieses Template ist im Bereich “Workflow” zu finden. Die automatisch angelegte “Activity1.xaml” kann gelöscht werden. Danach wird mit “Add-Item” eine neue Code-Activity in das Projekt eingefügt. Im Beispiel nenne ich diese “SimpleActivity.cs” Mit dieser Vorlage wird die Klasse “SimlpeActivity” angelegt und die Methode Execute überschrieben. 1 public sealed class SimpleActivity : CodeActivity 2 { 3 4 public InArgument<string> Text { get; set; } 5 public OutArgument<string> Result { get; set; } 6 7 protected override void Execute(CodeActivityContext context) 8 { 9 10 string text = context.GetValue(this.Text); 11 12 Result.Set(context, text.ToUpper()); 13 } 14 } In den Zeile 4 und 5 werden ein Input und ein Output Parameter für diese Activity angelegt. Innerhalb der Excecute Methode kann über Context.GetValue ein Inputparameter abgerufen werden. Mit Result.Set kann der Output-Parameter gesetzt werden. In diesem simplen Demo wird der Inputwert in Großbuchstaben in den Output geschrieben. In der Realität wird wohl die Execute-Methode etwas komplexer werden. Teil 2 – Das Deployment Das Assembly muss mit einem “strong name” versehen sein, daher muss das Assembly signiert werden. Dafür wird in den Properties des Projektes, die Checkbox “Sign the assembly” gesetzt und somit ein neues Keyfile im Projekt angelegt. Als nächstes muss eine Datei mit dem Namen “allowedtypes.xml” erstellt werden. In dieser Datei werden die Typen beschrieben, welche vom Workflowmanager geladen werden. Der Dateiinhalt sieht wie folgt aus: 1 <AllowedTypes> 2 <Assembly Name="SimpleActivityDemo"> 3 <Namespace Name="SimpleActivityDemo"> 4 <Type>SimpleActivity</Type> 5 </Namespace> 6 </Assembly> 7 </AllowedTypes> Nachdem das Projekt kompiliert wurde müssen die erstellte DLL und die Datei “AllowedTypes.xml” in ein Verzeichnis des Workflowmanagers kopiert werden. Das Zielverzeichnis ist: “C:\Program Files\Workflow Manager\1.0\Workflow\Artifacts” Möchte man die Activity nun in einem Visual Studio Workflow verwenden, muss die Activity in die Toolbox aufgenommen werden. Im Context-Menü der Toolbox kann mit dem Menü Punkt “Choose Items…” die DLL ausgewählt werden und damit die enthaltene Activity in die Toolbox aufgenommen werden. Danach kann die Custom-Activity wie jede andere in Workflow Projekte aufgenommen werden. Die beiden Parameter “Text” und “Result” sind wie gewohnt über das Eigenschaftenfenster im Visual Studio zu setzen. „Sie möchten mehr zur  Workflow Programmierung mit SharePoint erfahren? Werfen Sie doch mal einen Blick auf unser dazu passendes SharePoint Training.“

Was ist schneller? CAML oder KQL

Unlängst beschäftigte ich mich mit der Frage, welcher Weg der schnellste ist, um auf die Daten zu zugreifen: der Zugriff über eine CAML Abfrage oder der Zugriff über den Search Index. Daher habe ich eine SharePoint Kontakt-Liste mit ca. 18.000 Datensätzen gefüllt und dann eine simple Abfrage programmiert. Die Aufgabenstellung war die Suche im “CompanyName” mit dem BeginsWith Operator. Die CAML Variante: lbAusgabe.Items.Clear(); SPList firmen = SPContext.Current.Web.Lists["Firmen"]; DateTime start = DateTime.Now; int anz = 0; string caml = @"<Where><BeginsWith> <FieldRef Name='Company'/> <Value Type='Text'>{0}</Value> </BeginsWith></Where>"; SPQuery q = new SPQuery(); q.Query = string.Format(caml, txtSuche.Text); q.QueryThrottleMode = SPQueryThrottleOption.Override; q.RowLimit = 100; foreach (SPListItem it in firmen.GetItems(q)) { lbAusgabe.Items.Add(it["Company"].ToString() ?? ""); anz++; } DateTime ende = DateTime.Now; double Dauer = (ende - start).TotalMilliseconds; lblDauer.Text = "Dauer:" + Dauer.ToString("#,###"); lblAnzahl.Text = "Anzahl: " + anz.ToString(); Diese Abfrage liefert nur die ersten 100 Datensätze. Da die Abfrage aber über alle Datensätze läuft, ist es notwendig das List Throttling zu deaktivieren. Daher wird der QueryThrottleMode auf “Override” gestellt. Die zweite Variante verwendet die Keyword Query Language (KQL) und greift somit auf den Search-Index zu: int anz = 0; lbAusgabe.Items.Clear(); SPList firmen = SPContext.Current.Web.Lists["Firmen"]; DateTime start = DateTime.Now; KeywordQuery q = new KeywordQuery(); q.QueryText = "CompanyOWSTEXT:" + txtSuche.Text + "*"; q.QueryText += " AND Path:" + firmen.ParentWeb.Site.Url + firmen.ParentWebUrl + firmen.RootFolder + "*"; q.SelectProperties.Add("Company"); q.RowLimit = 100; SearchExecutor se = new SearchExecutor(); ResultTableCollection rtc = se.ExecuteQuery(q); var resultTables = rtc.Filter("TableType", KnownTableTypes.RelevantResults); var resultTable = resultTables.FirstOrDefault(); foreach(DataRow dr in resultTable.Table.Rows) { lbAusgabe.Items.Add(dr["Company"].ToString() ?? ""); anz++; } DateTime ende = DateTime.Now; double Dauer = (ende - start).TotalMilliseconds; lblDauer.Text = "Dauer:" + Dauer.ToString("#,###"); lblAnzahl.Text = "Anzahl: " + anz.ToString();   Spannend war die Messung der Abfragezeiten. Um valide Ergebnisse zu bekommen, habe ich jede Abfrage mehrmals ausgeführt. Hier die Ergebnisse in Millisekunden:   CAML Search Abfrage 1 413 138 Abfrage 2 423 201 Abfrage 3 449 109 Abfrage 4 425 113 Abfrage 5 472 105 Abfrage 6 459 112 Mittelwert 440,2 129,7   Hier ist eindeutig zu erkennen, dass der Zugriff über die Search deutlich schneller ist. Allerdings wurde in der Liste kein Index gesetzt. Sobald in der Kontakt-Liste auf das Feld “CompanyName” ein Index gesetzt wird, sieht das Ergebnis anders aus:   CAML Search CAML Mit index Abfrage 1 413 138 34 Abfrage 2 423 201 23 Abfrage 3 449 109 27 Abfrage 4 425 113 26 Abfrage 5 472 105 27 Abfrage 6 459 112 30 Mittelwert 440,2 129,7 27,8   Und hier sieht man ganz klar, warum es empfehlenswert ist einen Index zu verwenden. Durch diese simple Konfiguration kann die Abfragezeit deutlich reduziert werden! SharePoint ist keine Datenbank! Wenn die SharePoint-Listen aber dennoch als Datenbank verwendet werden, dann sollte man wie in jeder richtigen Datenbank auch Indizes verwenden! Diese und weitere Varianten im Umgang mit SharePoint-Listen per Programmcode werden auch in unserer Schulung “SharePoint 2013 - Lösungen entwickeln und anpassen" behandelt.

Event-based Async-Pattern in SharePoint hosted Apps mit TypeScript

TypeScript erlaubt richtige Software-Entwicklung im JavaScript-Umfeld. Klassen, Vererbung, Interfaces und Polymorphie sind nun auch in der Webentwicklung möglich. In Verbindung mit dem SharePoint Client Object Modell kann TypeScript auch genutzt werden, da seit einiger Zeit umfangreiche Type-Definition-Dateien existieren. Allerdings beruht das CSOM auf einer asynchronen Ausführung des Codes. Sobald versucht wird, die Datenzugriffe auf SharePoint in TypeScript-Klassen zu verpacken, um mehr Übersichtlichkeit im Code zu erhalten, steht man vor dem Problem ein Asynchronous Pattern (z.B: Event-based Asynchronous Pattern (EAP))  zu implementieren. Der Kernpunkt hierfür sind Events. Allerdings müssen diese in TypeScript erst selbst implementiert werden, da die Sprache hierfür noch keine Unterstützung anbietet. Meine Implementation eines Events baut auf einer internen Invokation-List auf. Ein Array von Funktionen mit einer simplen Signatur. Um flexibel zu bleiben, werden Generics verwendet. Somit kann ich als Parameter jedes Objekt übergeben. Dann noch drei public-Functions um Handler zu registrieren, abzumelden und um das Event auszulösen. Fertig ist meine Event-Klasse 1: module Events { 2: export class EventClass<T> { 3: private functions: { (param?: T): void; }[] = []; 4: public add(func: { (param?: T): void; }) { 5: this.functions.push(func); 6: } 7:   8: public remove(func: { (param?: T): void; }) { 9: this.functions = this.functions.filter(f => f !== func); 10: } 11:   12: public fireEvent(param?: T) { 13: if (this.functions) { 14: this.functions.forEach(f => f(param)); 15: } 16: } 17: } 18: }   Die SharePoint-Seite Das Ziel ist, eine typische SharePoint-Aktion, wie z.B. einen Task in einer Taskliste zu erstellen, in eine Klasse zu verpacken. Die Liste liegt im HostWeb. Um das Handling mit dem ClientContext für das Host Web einfacher zu gestalten, sind zunächst zwei Hilfsklassen sinnvoll: 1: class Url { 2: public static getUrlVars () : string[] { 3: var vars:string[] = [], hash; 4: var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&'); 5: for (var i = 0; i < hashes.length; i++) { 6: hash = hashes[i].split('='); 7: vars.push(hash[0]); 8: vars[hash[0]] = hash[1]; 9: } 10: return vars; 11: } 12:   13: public static getUrlVar (name:string):string { 14: return this.getUrlVars()[name]; 15: } 16: } 17:   18: class SPContextHelper { 19: public hostContext: SP.AppContextSite; 20: constructor(public clientContext: SP.ClientContext) { 21: this.hostContext = this.getHostWebCtx(this.clientContext); 22: } 23:   24: private getHostWebCtx(ctx: SP.ClientContext): SP.AppContextSite { 25: var url = decodeURIComponent(Url.getUrlVar('SPHostUrl')); 26: return new SP.AppContextSite(ctx, url); 27: } 28: } Die genaue Erklärung, wie das mit dem Zugriff auf das Hostweb so läuft, gibt’s hier. Die Codezeilen, um einen Task anzulegen, habe ich in der Klasse “TaskHelper” gekapselt. Die Feldwerte des Tasks liegen in der Klasse “Task”. Der Taskhandler legt im Konstruktor einen Verweis auf die SharePoint-Liste an und initialisiert die vorher gezeigte SPContextHelper-Klasse. 1: class Task { 2: public Beschreibung: string; 3: public Faellig: Date; 4: } 5:   6: class TaskHandler { 7: private contextHelper: SPContextHelper; 8:   9: private liste: SP.List; 10:   11: public onSuccess: Events.EventClass<string> = new Events.EventClass<string>(); 12: public onError: Events.EventClass<string> = new Events.EventClass<string>(); 13:   14: constructor(public listenName: string) { 15: this.contextHelper = new SPContextHelper(SP.ClientContext.get_current()); 16: this.liste = this.contextHelper.hostContext.get_web().get_lists().getByTitle(listenName); 17: } 18:   19: public AddTaskToSharePoint(task: Task) { 20: var neuesItem: SP.ListItem = this.liste.addItem(new SP.ListItemCreationInformation()); 21: neuesItem.set_item('Title', task.Beschreibung); 22: neuesItem.set_item('DueDate', task.Faellig); 23: neuesItem.update(); 24:   25: this.contextHelper.clientContext.executeQueryAsync( 26: (sender, args) => { this.onSuccess.fireEvent('its OK');}, 27: (sender, args) => { this.onError.fireEvent(args.get_message());} 28: ); 29: } 30: } In Zeile 11 und 12 werden die beiden Events onSuccess und onError angelegt. Es sind public Objekte. Sie werden mit dem Type “string” angelegt, d.h. die Message des Events ist ein simpler String. In Zeile 26 und 27 sieht man das Auslösen des Events. Das sind die beiden Rückruf-Funktionen der executeQueryAsync-Methode des Client Context. Der App-Code Zu guter Letzt sollte dieser Code nun auch in der App verwendet werden. Die TaskHandler-Instanz wird global initialisiert. In der Document-Ready-Funktion werden die beiden EventHandler, OKHandler und ErrorHandler an die beiden Events gehängt. In der Funktion “addTask” wird dann nur noch die “AddTaskToSharePoint”-Methode des Handlers aufgerufen. 1: var taskHandler: TaskHandler = new TaskHandler("Aufgaben"); 2: $(document).ready(function () { 3: taskHandler.onSuccess.add(OKHandler); 4: taskHandler.onError.add(ErrorHandler); 5: }); 6:   7: function OKHandler(txt: string) { 8: alert(txt); 9: } 10:   11: function ErrorHandler(txt: string) { 12: alert(txt); 13: } 14:   15: function addTask() { 16: var titel: string = $('#txtBeschreibung').val(); 17:   18: var t: Task = new Task(); 19: t.Beschreibung = titel; 20: taskHandler.AddTaskToSharePoint(t); 21: }

ppedv-SharePoint Camp im SharePoint Podcast

im bekannten SharePoint Podcasts von Michael Greth findet unser SharePoint Camp Erwähnung. Das Camp bietet einen komprimierten Einstieg in das umfangreiche Produkt “SharePoint 2013”. Neben der Administration von SharePoint wird auch das Thema Lösungserstellung und Programmierung in nur fünf Tagen vermittelt. Als Trainer konnten namhafte Experten der SharePoint Community gewonnen werden. Die Erfahrungsberichte der Teilnehmer eines der letzten Camps können nun im Podcast abgerufen werden. Danke an Michael Greth für das Interview. Link zum Podcast

Fehlermeldung beim Veröffentlichen eines InfoPath-Formulars in SharePoint

Beim Veröffentlichen von InfoPath-Formularen kann es zu der Fehlermeldung “Das folgende Formular kann durch InfoPath nicht gespeichert werden….” kommen. Leider ist diese Fehlermeldung nicht hilfreich. Dieses Problem tritt meist dann auf, wenn InfoPath auf der Servermaschine verwendet wird. Arbeitet man remote und nicht direkt am SharePoint-Server tritt dieses Problem nicht auf. Das Problem lässt sich leicht lösen. Es muss das Windows Feature “Desktop Experience”, welches im Bereich “User Interfaces and Infrastructure” angesiedelt ist, aktiviert sein. Und dieses Feature ist auf dem Server-Betriebssystem nicht per default aktiv. Daher tritt die Fehlermeldung meist am SharePoint-Server selbst auf PS: auch bei der Arbeit mit Office auf der Servermaschine ist dieses Feature notwendig.

SharePoint-Listenelemente nach Gruppenzugehörigkeit filtern

In SharePoint ist es bekanntermaßen sehr einfach möglich, sich eigene Abfragen in Form von Listenansichten auf der Weboberfläche “zusammen zu customizen”. In wenigen Sekunden kann man so nur noch die Listenelemente anzeigen lassen, die einer bzw. mehreren bestimmten Bedingungen entsprechen. Ein oft genutztes Feature stellt die Möglichkeit dar, sich nur seine eigenen Items bzw. Dokumente anzeigen zu lassen, indem z.B. bei einer Aufgabenliste der Filter “Assigned to” = “[Me]” (also diejenigen Aufgaben, die mir zugewiesen sind) angewendet wird. Genau an dieser Stelle ergeben sich interessante Möglichkeiten, denn häufig ist der einzelne Benutzer als Filterkriterium aus Gründen der Wartbarkeit und Wiederverwendbarkeit nicht wirklich praktikabel und schon ist der Wunsch nach einem Filter auf Gruppenebene geboren. So sollen bspw. Benutzer einer bestimmten Gruppe (z.B. einer Abteilung) innerhalb einer Aufgabenliste nur diejenigen Aufgaben angezeigt bekommen, die der entsprechenden Gruppe zugewiesen sind. Leider gibt es dafür out of the Box keine Filterfunktion im Rahmen der Customizingmöglichkeiten der Weboberfläche. Dieser Blogeintrag beschreibt einen relativ einfachen Weg, diese Filtermöglichkeit nach Gruppenzugehörigkeit bereitzustellen. Dabei muss die Listenansicht, genauer das CAML (Collaborative Application Markup Language) Statement des darin befindlichen ListViewWebParts angepasst werden, z.B. mit Hilfe des SharePoint Designers. Als erstes muss der SharePoint Designer mit der entsprechenden Website verbunden werden und dort die passende Liste geöffnet werden. Ggf. ist nun eine neue Listenansicht anzulegen (alternativ kann auch eine bestehende angepasst werden). Im Bsp. sei dies die “OurGroupTasks” – also eine Ansicht mit allen Aufgaben für die Gruppe, welcher der aktuell angemeldete Benutzer zugehörig ist.    Diese neu angelegte Listenansicht muss nun geöffnet und im asp-Code nach der Stelle gesucht werden, wo die Abfrage definiert wird. Dazu STRG+F –> Suchen nach “Query”. Das Ergebnis sollte der Cursor direkt im Code markiert darstellen. Wenn zuvor eine neue Listenansicht erzeugt wurde, sollte der Query-Tag leer sein, wie im Beispiel. Dieser Query-Tag muss nun - entsprechend der Logik “WENN zugewiesen an aktuelle Benutzergruppe” - angepasst werden. Achtung: in dem Fall werden wirklich nur die Aufgaben angezeigt, die der Gruppe zugewiesen sind: <Query> <Where> <Membership Type="CurrentUserGroups"> <FieldRef Name="AssignedTo"/> </Membership> </Where> </Query> Optional: Sollen auch zusätzlich zu den gemeinsamen Gruppenaufgaben noch die Aufgaben angezeigt werden, die dem Benutzer direkt zugewiesen sind, so ist die CAML Abfrage um eine Bedingung zu erweitern, so dass die Logik “WENN zugewiesen an aktuelle Benutzergruppe ODER Zugewiesen an = Meine UserID” zutrifft: <Query> <Where> <Or> <Membership Type="CurrentUserGroups"> <FieldRef Name="AssignedTo"/> </Membership> <Eq> <FieldRef Name="AssignedTo"/> <Value Type="Integer"> <UserID/> </Value> </Eq> </Or> </Where> </Query>   Wenn jedoch nicht nach der CurrentUserGroup sondern einer ganz bestimmten Gruppe gefiltert werden soll, muss die ID der gewünschten Gruppe bekannt sein. Diese GroupID lässt sich unter den Site Settings –> People & Groups –> Gewünschte Gruppe in der Url ablesen: Soll nun der Filter für eine bestimmte Gruppe gesetzt werden, ist die folgende CAML Query zu verwenden (im Bsp. wurde dazu eine Ansicht “SpecificGroupTasks” angelegt, welche nach Aufgaben der SP Gruppe mit der ID 8 filtert): <Query> <Where> <Membership Type="SPGroup" ID="8"> <FieldRef Name="AssignedTo"/> </Membership> </Where> </Query> Hierbei ist zu beachten, dass über diesen Weg alle direkt an Benutzer (die sich in dieser Gruppe befinden) zugewiesene Aufgaben zurück kommen, nicht aber die Aufgaben, die der Gruppe selbst zugewiesen sind). Um Aufgaben, die einer bestimmten Gruppe zugewiesen sind anzeigen zu lassen, muss folgende Bedingung verwendet werden (wichtig ist dabei, dass das Property LookupId auf true gesetzt wird, sonst kann nicht auf die ID der Gruppe verwiesen werden!): <Query> <Where> <Eq> <FieldRef Name="AssignedTo" LookupId="true" /> <Value Type="Integer">8</Value> </Eq> </Where> </Query> Natürlich können diese beiden Bedingungen entsrechend verknüpft werden. Dann käme folgendes Statement zustande gemäß der Logik “WENN zugewiesen an Mitglied in Gruppe mit ID 8 ODER zugewiesen an = 8”: <Query> <Where> <Or> <Membership Type="SPGroup" ID="8"> <FieldRef Name="AssignedTo"/> </Membership> <Eq> <FieldRef Name="AssignedTo" LookupId="true" /> <Value Type="Integer">8</Value> </Eq> </Or> </Where> </Query> Nun muss die Page nur noch gespeichert und im Browser erneut geladen werden, damit die Änderungen sichtbar sind. Im Beispiel als Vergleich – Ansicht “Alle Aufgaben” (Bild A) vs. Ansicht “OurGroupTasks” (Bild B) vs. “OurGroupTasks” mit Anzeige der eigenen Aufgaben des Benutzers (Bild C):         Im Vergleich dazu die Listenansichten der zweiten Variante – das Anzeigen von Aufgaben von Benutzern einer bestimmten Gruppe (Bild D), von Aufgaben der Gruppe selbst (Bild E) sowie eine Verknüpfung beider Ergebnismengen (Bild F):       Dieses Beispiel funktioniert natürlich auch mit SharePoint 2007 und 2010.