Using NotesUI* Classes in C# with COM

The NotesUI* Classes are not accesible in Visual Studio via the References-Dialog. Also in the DesignerHelp it is said, that you can not use the UI-Classes.

 

But there is a very simple Solution in C# by using the dynamic – Keyword and the .NET System.Activator Class.

 

The following Code-Sample connects to an running Notes Client and performs some actions:

class Program
    {
        static void Main(string[] args)
        {
            dynamic session = Activator.CreateInstance(Type.GetTypeFromProgID("Notes.NotesSession"));
            dynamic uiws = Activator.CreateInstance(Type.GetTypeFromProgID("Notes.NotesUIWorkspace"));
            dynamic uidb = uiws.CurrentDatabase;
            dynamic dbDir = session.GetDbDirectory("NTSDEV2008");

            Console.WriteLine("Benutzer : {0}", session.UserName);
            Console.WriteLine("Datenbank: {0}", uidb.Database.FilePath);



            uiws.OpenDatabase("NTSDEV2008", "names.nsf");

            uiws.URLOpen("https://www.google.at");
                       
            


            Console.ReadKey(); 
        }
     }

 

I found no current usecase for this at the moment, but good to know that this is possible …

Convert Lotus Notes Documents to PDFs with LotusScript and Microsoft Word

In some of our old databases we wanted to convert the NotesDocument itself to a PDF-A Documents for archiving purposes.

We did it until now by opening the NotesUIDocument in the client and converted it ot PDF-A with an PDF-Printer (PDF-Creator). But often these NotesUIDocuments did note closed correctly and at the end of the NotesAgent we had thousands of NotesUIDocuments opened in the Client…

We searched for another way to convert the NotesDocument. We tried to render it to an NotesRichtTextItem, converting it to an MimeItem (based on the work from Yuri (here…), generating some HTML. But the HTML had some glitches: Notes inserts a small transparent gif into empty table-cells, so we would have to deal with different MimeEntities wich is also a PITA.

Then I found some code (from Steven J. Knight) in the IBM-Forums wich called a Notes-API function to export Microsoft RTF (which is called by the @Command([ExportFile], „Microsoft RTF“, fileName)

This works as a charm, generating nice looking RTF-Files. Together with a Microsoft Word COM-Instance we convert it to PDA-A quite fast and reliable:

%REM
  Library PdfConverter
  Created 11.07.2013 by Harald Reisinger
  Description: Functions to generate RTF and PDF Documents from NotesDocuments
  Needs the OpenLog Databse for Logging Errors (http://www.openntf.org/internal/home.nsf/project.xsp?action=openDocument&name=OpenLog)
%END REM
Option Public
Option Declare

Use "OpenLogFunctions"

Const APIModule = "NNOTES" 

Const wdExportFormatPDF = 17
Const wdExportFormatXPS = 18
Const wdExportOptimizeForOnScreen = 1
Const wdExportOptimizeForPrint = 0
Const wdExportAllDocument = 0
Const wdExportCurrentPage = 2
Const wdExportFromTo = 3
Const wdExportSelection = 1
Const wdExportDocumentContent = 0
Const wdExportDocumentWithMarkup = 7
Const wdExportCreateHeadingBookmarks = 1
Const wdExportCreateNoBookmarks = 0
Const wdExportCreateWordBookmarks = 2

Declare Function MailGetMessageBodyComposite Lib APIModule Alias "MailGetMessageBodyComposite" ( ByVal hNT As Long, ByVal N As String, ByVal D As String, nD As Long) As Integer
Declare Function ExportRTF Lib "nxrtf" Alias "ExportRTF" (ByVal sTempFile As String, ByVal flags As Long, hmod As Long, ByVal altlibrary As String, ByVal sRTFFile As String) As Integer


%REM
  Sub ConvertDocToRtfFile
  Description: Converts a NotesDocument to an RTF-Document and saves it under the filePath
  Generates and removes some temporary Files while Working
%END REM
Public Sub ConvertDocToRtfFile(doc As NotesDocument, filePath As String)
  Dim tempDoc As NotesDocument
  Dim tempRti As NotesRichTextItem

  On Error GoTo ErrorHandler

  Set tempDoc = doc.ParentDatabase.CreateDocument()
  Set tempRti = tempDoc.CreateRichTextItem("Body")
  
  Call doc.RenderToRTItem(tempRti)
  Call ConvertItemToRtfFile(tempRti, filePath)	

  Exit Sub

ErrorHandler:
  Call LogErrorEx("Error in: ConvertDocToRtfFile", SEVERITY_HIGH, doc)
  Error Err, Error$


End Sub
%REM
  Sub ConvertDocumentToPdf
  Description: Converts a NotesDocument to an PDF-Document and saves it under the filePath
  Needs an COM-Instace of a Word Application created with:
    Dim word As Variant
    Set word = CreateObject("Word.Application")
  Generates and removes some temporary Files while Working
%END REM
Public Sub ConvertDocumentToPdf(doc As NotesDocument, word As Variant, filePath As String)
  Dim wordDoc As Variant
  
  On Error GoTo ErrorHandler

  Call ConvertDocToRtfFile(doc, filePath & ".rtf")
  'http://msdn.microsoft.com/en-us/library/office/bb216319(v=office.12).aspx
  Set wordDoc = word.Documents.Open(filePath & ".rtf", False, True, False)
  'http://msdn.microsoft.com/en-us/library/office/bb256835(v=office.12).aspx
  Call wordDoc.ExportAsFixedFormat(filePath, wdExportFormatPDF, False, wdExportOptimizeForPrint, wdExportAllDocument, 0, 9999999, wdExportDocumentContent, True, True,  wdExportCreateHeadingBookmarks, True, True, True, Nothing)
  Call wordDoc.Close(0)

  Kill filePath & ".rtf"

  Exit Sub

ErrorHandler:
  Call LogErrorEx("Error in: ConvertDocumentToPdf", SEVERITY_HIGH, doc)
  Error Err, Error$

End Sub

%REM
  Function ConvertItemToRtfFile
  Description: Converts an NotesRichTextItem to an RTF-Document and saves it under the filePath
  Generates and removes a temporary File while Working
%END REM
Public Sub ConvertItemToRtfFile(item As NotesRichTextItem, filePath As String)
  On Error GoTo ErrorHandler
  
  Dim fileSize As Long
  Dim doc As NotesDocument
  
  Set doc = item.Parent

  Call MailGetMessageBodyComposite(doc.handle , "Body", filePath & ".cd", fileSize) 
  Call ExportRTF(filePath & ".cd", 0, 0, "", filePath)
  
  Kill filePath & ".cd"

  Exit Sub

ErrorHandler:
  Call LogErrorEx("Error in: ConvertItemToRtfFile", SEVERITY_MEDIUM, doc)
  Error Err, Error$
End Sub

 

Sparse Matrix in C#

Hin und wieder will man wieder etwas einfacheres machen und etwas, dass es schon 100.000mal auf dieser Welt gibt auch wieder neu erfinden. Ich hoffe ich habe jetzt hier nicht irgendein Softwarepatent gröber verletzt, wenn ja – mea culpa.

Die Sparse-Matrix hat gegenüber einem Array vor allem Vorteile, wenn man nur wenige Elemente der Matrix (bzw. des Arrays) belegt, da wäre eine Array eine Speicherverschwendung (überhaupt bei grossen Dimensionen). Anbei meine einfache Implementierung, ich habe bewusst nur eine einfache .NET Liste mit Tupeln verwendet, zur Abfrage der Liste verwende ich einfach LINQ (es ist einfach zu praktisch…) und hab noch eine kleine API draufgesetzt.

Sicherlich gäbe es auch einen effizientere Datenstrukturen dafür, aber ich denke für die meisten Fälle wird wohl die einfache Liste ausreichend sein, bzw. müsste man dann ja auch noch Vergleichsmessungen anstellen.

Mit dabei sind auch ein paar klitzekleine Tests, natürlich hätte die man auch als Unit-Tests auch ausführen können, aber sooo viel Bock hatte ich dann auch nicht.

using System;
using System.Collections.Generic;
using System.Linq;

namespace SparseMatrix {
  class Program {
    static void Main(string[] args) {

      var sm = new SparseMatrix();

      sm.SetAt(5, 6, "Hallo");
      sm.SetAt(6, 7, "Welt!");

      Console.WriteLine(sm.GetAt(5, 6));
      Console.WriteLine(sm.GetAt(6, 7));

      sm.SetAt(6, 7, "Harald");

      Console.WriteLine(sm.GetAt(6, 7));
      Console.WriteLine("RowCount row=7: {0}", sm.GetRowCount(6));
      Console.WriteLine("ColCount col=6: {0}", sm.GetColumnCount(7));

      Console.WriteLine("Entferne Element auf row=6 / col=7");
      sm.SetAt(6, 7, null);
      Console.WriteLine("RowCount row=7: {0}", sm.GetRowCount(6));
      Console.WriteLine("ColCount col=6: {0}", sm.GetColumnCount(7));

      sm[6, 6] = "Blogleser!";

      List results = sm.GetRowData(6).ToList();
      // Sollte "Hallo Blogleser " ausgeben
      foreach (string s in results) Console.Write("{0} ", s);
      Console.WriteLine();

      Console.WriteLine("Auf Postion 5/6 steht {0}", sm[5, 6]);


      Console.ReadLine();


    }
  }
  
  public class SparseMatrix {
    private List<Tuple<int, int, T>> data = new List<Tuple<int, int, T>>();

    private Tuple<int, int, T> getTuple(int row, int col) {
      return data.Where(e => e.Item1 == row && e.Item2 == col).FirstOrDefault();
    }

    public T GetAt(int row, int col) {
      var e = getTuple(row, col);
      if (e == null) {
        return default(T);
      } else {
        return e.Item3;
      }
    }

    public void RemoveAt(int row, int col) {
      var e = getTuple(row, col);
      if (e != null) data.Remove(e);
      return;
    }

    public void SetAt(int row, int col, T value) {
      var e = getTuple(row, col);
      if (e == null && EqualityComparer.Default.Equals(value, default(T))) {
        return;
      }
      if (e != null && EqualityComparer.Default.Equals(value, default(T))) {
        data.Remove(e);
        return;
      }
      if (e == null) {
        data.Add(new Tuple<int, int, T>(row, col, value));
        return;
      }

      data.Remove(e);
      data.Add(new Tuple<int, int, T>(row, col, value));
    }

    public IEnumerable GetRowData(int row) {
      return data.Where(e => e.Item2 == row).Select(e => e.Item3);
    }

    public IEnumerable GetColumnData(int col) {
      return data.Where(e => e.Item1 == col).Select(e => e.Item3);
    }

    public int GetColumnCount(int col) {
      return data.Where(e => e.Item2 == col).Count();
    }

    public int GetRowCount(int row) {
      return data.Where(e => e.Item1 == row).Count();
    }

    public T this[int row, int col] {
      get {
        return GetAt(row, col);
      }
      set {
        SetAt(row, col, value);
      }

    }


  }
}

 

Visual Studio Lightswitch 2010 – Einen Search Screen bei Änderungen automatisch aktualisieren.

Eine Applikation mit Visual Studio Lightswitch zu machen, ist eine spaßige Angelegenheit, man kommt schnell voran weil man sich nicht um die Infrastruktur der Applikation kümmern muss, aber trotzdem eine schöne, mehrschichtige Anwendung bekommt. Der Anfänger kann leicht einfache Use-Cases umsetzen, die Fortgeschrittenen können sich viel anpassen und auch erweitern.

Ein kleines Manko, welches mir bei der Umsetzung einer Anwendung aufgefallen ist (bzw. deren Anwender):

Man hat eine Listenansicht (SearchScreen) die eine bestimmte Menge an Datensätzen anzeigt. Fügt man nun in einen anderen Bildschirm (z.B. einer Detailansicht (Details Screen) oder einer Maske zum Hinzufügen (Add New Screen)) einen neuen Datensatz zu dieser Menge hinzu, schließt den Bildschirm und kehrt wieder zur Listenansicht zurück, so stellt man fest, dass der neue Datensatz nicht angezeigt wird, erst nachdem man auf die standardmäßig vorhandene Schaltfläche „Aktualisieren“ drückt, erscheinen die neuen Daten.

Ich möchte hier einen Weg vorstellen wie das auch automatisch funktioniert, erfunden habe es aber nicht ich, sondern Sheel Shah vom Visual Studio Lightswitch Team. Den Originalbeitrag findet ihr hier. Die Idee daran ist es, das ganze über einen eigenen EventHandler abzuhandeln. Das heisst, die Applikation bekommt einen Eventhandler verpasst, der Search Screen registriert sich für alle Events dieses Handlers. Wird nun in einem anderen Screen ein Datensatz hinzugefügt, oder verändert, so wird beim speichern dieses Event ausgelöst. Der Search Screen bekommt das Event mitgeteilt und kann damit die Datenquelle aktualiseren. Hier greifen dann die normalen Lightswitch-Mechanismen und die GUI wird aktualisiert.

Der einzige zu Sheel’s Beitrag ist, dass ich bei mir einen Search Screen aktualisiere, und kein einzelnes Steuerelement.

Ausgangslage, die Tabelle mit den Namen „Kunden“:

Tabelle Kunden - Übersicht
Tabelle Kunden

Ausgehend von dieser Tabelle gibt es zum Einem den Search Screen „Kunden“ sowie die Eingabemaske zum Anlegen eines neuen Kunden „Neuer Kunde“

Beginnen wir nur nun mit den Adaptionen an der Eingabemaske „Neuer Kunde“, die erste Änderung muss im Code an der Stelle „KundeNeu_CanRun“ vorgenommen werden.

Bearbeiten von "KundeNeu_CanRun"
Bearbeiten von „KundeNeu_CanRun“

Folgende Anpassungen sind dort zu machen: Anlegen eines Delegaten und eines Eventhandlers, sowie einer Methode die das Event „feuert“.

public delegate void RecordAdded();
public event RecordAdded CustomerAdded;

public void RaiseCustomerAdded() {
  if (this.Details.Dispatcher.CheckAccess()) {
    CustomerAdded();
  } else {
    this.Details.Dispatcher.BeginInvoke(delegate() { CustomerAdded(); });
  }
}

Wir bleiben im Detailformular, jetzt wird etwas Code zum Ereignis „KundeNeu_Saved“ hinzugefügt, die Ergänzung ist einfach, es wird einfach das Event „abgefeuert“:

partial void KundeNeu_Saved() {
  this.Close(false);
  Application.RaiseCustomerAdded();
}

So, wir haben nun den Event-Handler definiert, wir haben dafür gesorgt, dass das Event auch ausgelöst wird, nun brauchen wir noch jemanden, der sich für diese Events auch interessiert, und das ist nun das Suchformular „Kunden“. In dieser Ansicht gibt es oben im Ribbon eine Schaltfläche, mit der man einen Kunden neu anlegen kann – hier registriert man sich für das Event:

partial void NewCustomer_Execute() {
  Application.CustomerAdded += new Application.RecordAdded(CustomerAdded);
  Application.ShowKundeNeu();
}

Das ist die Methode, die dann aufgerufen wird, es wird einfach ein Refresh der Datenquelle vorgenommen, die dem Search Screen zugrundeliegt. Der Rest wird dann vom internen Eventsystem abgehandelt, und die GUI initialisiert.

private void CustomerAdded() {
  this.Details.Dispatcher.BeginInvoke(() => this.CustomersByName.Refresh());
}

Zu guterletzt sollte man auch noch aufräumen, ich denke aber, dass diese Lösung hier noch nicht unbedingt sauber ist, das muss ich mir bei Gelegenheit noch genauer ansehen, wenn der Search Screen geschlossen wird, wird der der Eventhandler wieder „deregistriert“:

partial void Kunden_Closing(ref bool cancel) {
  Application.CustomerAdded -= new Application.RecordAdded(CustomerAdded);
}

Kurzes Fazit: Die Implementierung ist doch ein wenig aufwändig, vor allem wenn man die Mechanismen des Event-Handlings noch nicht so gut versteht, aber es ist eine saubere und zuverlässige Umsetzung.