Montag, 30. Oktober 2017

Nintex Inline-functions ausprobiert

Nintex verfügt über "inline functions", mit denen die übergaben einiger Workflow-Schritte (Schritte, die das Erfassen einer Referenz erlauben) noch angepasst werden kann.
Diese inline-functions sind recht schnell und einfach selber erstellt - dafür gibt es z.B. hier und hier Anleitungen.
Die beschriebenen Schritte sind zusammengefasst wie folgt:

  • eine statische Methode bereitstellen, für die Verarbeitung
  • die entsprechende DLL auf alle Nintex-Server im GAC installieren
  • Einen Funktionsalias in Nintex registrieren (über NWAdmin.exe bzw. Add-InlineFunction
Mehrere Dinge finde ich an dem Vorgehen nicht optimal:
  • Warum müssen die DLLs auf allen Servern manuell in den GAC installiert werden
  • Warum muss mit einem weiteren Tool der Funktionsalias registriert werden?
Bei der Überlegung wie man dies in einen automatischen Prozess überführen kann - am besten so, dass ein Standard-SharePoint-Admin sich "wie zuhause" fühlt hatte ich die folgende Überlegung:
In diesem Fall würde das Deployment einer inline-function sich auf das bekannte "Add-SPSolution" & "Install-SPSolution" beschränken.
Für die Registrierung würde ich ein verstecktes Farm-Feature verwenden und die eigentliche Registrierung in SPFeatureReceiver.FeatureInstalled durchführen. (Bzw. die De-Registrierung dann in SPFeatureReceiver.FeatureUninstalling)

Es bleibt die Frage danach wie man denn in einem Event-Receiver den Funktionsalias an Nintex registriert. Die Antwort war recht schnell gefunden: Die Klasse Nintex.Workflow.Common.StringFunctionInfoCollection verfügt über die statische Methode Add.

public static void Add(string FunctionAlias, string AssemblyName, string NameSpace, string TypeName, string MethodName, string description, string usage, int lcid, bool hidden)

Die Parameter entsprechen 1:1 denen von NWAdmin.exe - mit der Ergänzung, dass die LCID -1 ist, wenn sie in NWAdmin nicht gesetzt wird.

Auf diese Weise lässt sich Nintex einfach um inline-functions erweitern und das Ergebnis ist eine "schöne", SharePoint-Konforme Lösung, die auch jeder kritischen Überprüfung eines Kunden standhalten kann...

Ergänzen muss ich noch etwas zu der Sprache/LCID die bei der Registrierung verwendet wird.

  1. Die Doku behauptet "Duplicate function aliases are not allowed, even if the locale identifiers are different." - dies ist nicht so: Ein Funktionsalias kann mehrfach, mit verschiedenen lcids registriert werden.
  2.  Die Anzeige der Inline-functions in Nintex ist lokalisiert, d.h. in einem Web der lcid 1031 werden auch nur (!) inline-functions angezeigt, die mit der lcid 1031 registriert wurden. In Mehrsprachigen Umgebungen ist es daher wichtig die inline-function für alle vorgesehenen Sprachen zu registrieren.
  3. Die Standard-Sprache (englisch, in Nintex keine lcid) wird nur angezeigt, wenn es keine einzige (!) lokalisierte inline-function gibt. Daraus folgt 1. dass man seine eigenen Funktionen nur lokalisieren sollte, für Sprachen die in Nintex bereits lokalisiert sind und 2. für die lcid 1033 sollte nie (!) eine Lokalisierung angelegt werden.

Mittwoch, 11. Oktober 2017

In VisualStudio SharePoint-Projekt die .wsp automatisch erstellen bei (re)Build

Ich mag es wenn meine wsp's aktuell sind. Ich finde es doof, nach einem (re)Build immer noch einmal "Publish" oder (mit CKSDev) "Package all SharePoint-Projects" manuell starten zu müssen. Zumal ich das dann ständig vergesse.... Daher versuche ich immer die .wsp's direkt nach dem Build erzeugen zu lassen.

Einfach in der csproj-Datei die folgende Zeile finden (i.d.R am Ende der Datei):
<Import Project="$(VSToolsPath)\SharePointTools\Microsoft.VisualStudio.SharePoint.targets" Condition="'$(VSToolsPath)' != ''" />

und anschließend die folgenden Zeilen anfügen (und damit die "dependencies" für den Build-Schritt ändern):

<PropertyGroup>
 <BuildDependsOn>
  $(BuildDependsOn);
  CreatePackage;
 </BuildDependsOn>
</PropertyGroup>

formatierten Text an ein Word-Dokument anfügen

Die Aufgabe des Tages heute: Text mit Formatierungen (vorzugsweise HTML) an ein Word-Dokument anfügen.

Die Lösung ist eigentlich recht einfach:
Das HTML wird als AlternativeFormatImportPart dem Dokument hinzugefügt und dieser dann im Körper des Dokumentes am Ende hinzugefügt.
Bei Verwendung des OpenXmlSdk sieht das wie folgt aus:

// open some docx-document
using (var docx = WordprocessingDocument.Open(newDoc, true))
using (var htmlStream = new MemoryStream(htmlBytes))
{
  var mainDoc = docx.MainDocumentPart;
  // some id to use for adding first & referencing later
  var partId = string.Format("SPAdded{0:N}",Guid.NewGuid());

  // the new AlternativeFormatImportPart
  var part = mainDoc.AddAlternativeFormatImportPart(AlternativeFormatImportPartType.Html, partId);
  part.FeedData(htmlStream);

  // an AltChunk (referencing the AlternativeFormatImportPart by use of ID) 
  var partRef = new AltChunk {Id = partId};

  // add the AltChunk after the last element of the document
  var body = mainDoc.Document.Body;
  body.InsertAfter(partRef, body.Elements().Last());

  // save the document
  mainDoc.Document.Save();
}
In heutigen Fall musste das alles natürlich aus dem SharePoint erfolgen und das zu verändernde Dokument als neue Version im SharePoint abgelegt werden.
Meine Lösung ist ein WebService, der entsprechend (client-Seite, Workflow, etc...) verwendet werden kann.
"Fertig" sieht das dann wie folgt aus:

public void AppendContent(string urlToDocInSharePoint, string contentToAdd)
{
 if (string.IsNullOrEmpty(urlToDocInSharePoint))
 {
  throw new ArgumentException("file-url must be given.", "urlToDocInSharePoint");
 }
 if (string.IsNullOrEmpty(contentToAdd))
 {
  // nothing to do..
  return;
 }

 using (var site = new SPSite(urlToDocInSharePoint))
 using (var web = site.OpenWeb())
 {
  var webAppUrl = site.Url.Replace(web.ServerRelativeUrl, string.Empty);
  var siteRelativeDocumentUrl = urlToDocInSharePoint.Replace(webAppUrl, string.Empty);
  if (!siteRelativeDocumentUrl.StartsWith("/"))
  {
   siteRelativeDocumentUrl = "/" + siteRelativeDocumentUrl;
  }

  var file = web.GetFile(siteRelativeDocumentUrl);
  if (!file.Exists)
  {
   throw new FileNotFoundException(string.Format("The given file \"{0}\" in site \"{1}\" with relative url \"{2}\" could not be found. No such file exists.", 
    urlToDocInSharePoint, site.Url, siteRelativeDocumentUrl));
  }

  if (!file.InDocumentLibrary)
  {
   throw new ArgumentException(string.Format("The given file \"{0}\" is not in a DocumentLibrary. Unable to modify.", siteRelativeDocumentUrl), "context");
  }

  var html = contentToAdd;
  if (!html.StartsWith("<html", StringComparison.InvariantCultureIgnoreCase))
  {
   html = string.Format("<html>{0}</html>", html);
  }

  // according to https://stackoverflow.com/questions/18089921/add-html-string-to-openxml-docx-document the html-bytes must be UTF8-encoded with Preamble...
  var htmlBytes = new UTF8Encoding(true).GetPreamble().Concat(Encoding.UTF8.GetBytes(html)).ToArray();

  using (var newDoc = new MemoryStream())
  {
   // copy file from SP to memory
   using (var spStream = file.OpenBinaryStream(SPOpenBinaryOptions.None))
   {
    spStream.CopyTo(newDoc);
   }

   newDoc.Seek(0, 0);

   //open & modify docx
   using (var docx = WordprocessingDocument.Open(newDoc, true))
   using (var htmlStream = new MemoryStream(htmlBytes))
   {
    var mainDoc = docx.MainDocumentPart;

    // add html as "alternative format" to document, then reference it in the main body....
    var partId = string.Format("SPAdded{0:N}",Guid.NewGuid());

    var part = mainDoc.AddAlternativeFormatImportPart(AlternativeFormatImportPartType.Html, partId);
    part.FeedData(htmlStream);

    var partRef = new AltChunk {Id = partId};

    var body = mainDoc.Document.Body;
    body.InsertAfter(partRef, body.Elements().Last());

    mainDoc.Document.Save();
   }
   newDoc.Seek(0, 0);

   //push newDoc as new Version to list
   var folder = file.ParentFolder;
   if (folder.RequiresCheckout)
   {
    file.CheckOut();
   }

   var uploaded = folder.Files.Add(file.Url, newDoc, true);
   uploaded.Update();

   if (folder.RequiresCheckout)
   {
    uploaded.CheckIn("Updated via WebService", SPCheckinType.MajorCheckIn);
    uploaded.Publish("Updated via WebService");
   }
  }
 }
}
Das ganze hätte noch etwas generischer gefasst werden können - war aber so vorerst ausreichend.