WPF Over Data mit Caliburn.Micro – die Shell

 

Prolog

Für die Pflege von Daten scheint Forms Over Data die erste Wahl zu sein – aber wo bleiben dann die Freiheiten der Datenbindung, das Abkoppeln des Codes vom Oberflächendesign, das MVVM-Muster?

Alternativ könnte man mit Microsoft Visual Studio Lightswitch eine Oberfläche gestalten. Nun, vielleicht später mal. Inzwischen schaue ich, ob ich mir ein modulares Backend mit Hilfe der folgenden Zutaten zusammenbauen kann:

  • Caliburn.Micro zum Verwalten der WPF-Oberfläche
  • Castle Windsor als DI-Container
  • NHibernate als ORM
  • FluentNHibernate zur Konfiguration von NHibernate
  • Machine.Specifications als Testumgebung, in Verbindung mit Machine.Fakes und RhinoMocks.
  • log4net fürs Logging

Alle genannten Pakete sind über NuGet abrufbar.

 

Die Oberfläche

Wie soll nun dieses Backend genau aufgebaut sein? – Nun, das Hauptprogramm gibt den Rahmen vor. Es sollten sich verschiedene Module einklinken können, die wiederum aus Submodulen bestehen.

Die Icons zur Auswahl der Module sollen links untereinander aufgelistet werden (im Bild unten als A). Zusätzlich zum Icon soll noch ein Modulname eingeblendet werden. Wird eines dieser Elemente aktiviert, sollen die Submodule zur Auswahl angeboten werden (im Bild als B). Zusätzlich kann man noch einen kleinen Tooltip einblenden.

image

 

Das Projekt

Im ersten Step wird das Projekt nur aus 3 Komponenten bestehen:

  • Das Hauptprogramm Lucifer.Office mit der Shell und dem Bootstrapper für Caliburn.Micro
  • Eine Klassenbibliothek Lucifer.Caliburn mit den Kontrakten und dem Logging-Mechanismus für CM
  • Dem Theme der Anwendung als Klassenbibliothek Lucifer.Theme

Wie der Screenshot beweist, könnte ich vor allem im letzten Punkt noch durchaus Hilfe gebrauchen 🙂

 

Lucifer.Caliburn – Interfaces

Als erstes braucht CM natürlich die Definition einer Shell. Glücklicherweise ist die überschaubar:

using Caliburn.Micro;

namespace Lucifer.Caliburn
{
    public interface IShell : IConductor
    {
    }
}

Dann hatten wir gesagt, was wir alles von einem einklinkbaren Modul wissen müssen. Daraus ergibt sich folgendes Interface:

using Caliburn.Micro;

namespace Lucifer.Caliburn
{
    public interface IModule : IScreen
    {
        string ModuleName { get; }
        string IconFileName { get; }
        string ToolTip { get; }
    }
}

Ein Screen ist es, weil CM einem durch das Aktivieren und Deaktivieren schon jede Menge Arbeit abnimmt.

Damit sind die nötigen Verträge schon abgearbeitet. Was noch fehlt, ist noch ein wenig Hilfe bei der Fehlersuche. Caliburn.Micro hat eine Logging-Schnittstelle, die einfach nur aktiviert werden muß. Man könnte sie mit dem normalen Logging zusammenlegen, hilfreicher dürfte jedoch die direkte Ausgabe im Debug-Fenster von Visual Studio sein. Deshalb folgende Implementierung:

using System;
using System.Diagnostics;
using System.Globalization;
using Caliburn.Micro;

namespace Lucifer.Caliburn
{
    public class CaliburnLogger : ILog
    {
        Type _type;

        public CaliburnLogger(Type type)
        {
            _type = type;
        }

        public void Info(string format, params object[] args)
        {
            Debug.WriteLine(CreateLogMessage(format, args), "INFO");
        }

        public void Warn(string format, params object[] args)
        {
            Debug.WriteLine(CreateLogMessage(format, args), "WARN");
        }

        public void Error(Exception exception)
        {
            Debug.WriteLine(CreateLogMessage(exception.ToString()), "ERROR");
        }

        string CreateLogMessage(string format, params object[] args)
        {
            return string.Format(CultureInfo.CurrentCulture, 
                "[{0}] {1} - [{2}]",
                DateTime.Now.ToString("o", CultureInfo.CurrentCulture),
                string.Format(format, args),
                _type);
        }
    }
}

Das war die Klassenbibliothek Lucifer.Caliburn, es folgt bereits das Hauptprogramm.

 

Lucifer.Office

Das Hauptprogramm ist ebenso schnell aufgebaut, da man sich nur um die folgenden Elemente kümmern muß:

  • Der Bootstrapper
  • Eine ShellView zur Definition des Hauptfensters
  • Das Model zur ShellView für den korrekten Aufbau der Elemente
  • Eine App.Xaml für das Bekanntmachen des Boostrappers und das Zusammenführen der Resource-Dictionaries

 

Der Bootstrapper

Wie eingangs erwähnt, soll Castle Windsor diesen Part übernehmen. Hierfür muß der von Caliburn.Micro erwartete Bootstrapper implementiert bzw 3 Methoden überschrieben werden: GetInstance, GetAllInstances sowie Configure. Darüber hinaus erweitern wir Configure derart, daß (später) alle benötigten Module eingelesen werden.

Daraus ergibt sich folgende Klasse:

namespace Lucifer.Office { public class CastleBootstrapper : Bootstrapper<IShell> { IWindsorContainer _container; protected override object GetInstance(Type serviceType, string key) { var export = string.IsNullOrEmpty(key)

? _container.Resolve(serviceType) : _container.Resolve(serviceType, key); if (export != null) return export; var contract = string.IsNullOrEmpty(key) ? serviceType.Name : key; throw new Exception(string.Format("Could not locate any instances of contract {0}.", contract)); } protected override IEnumerable<object> GetAllInstances(Type service) { yield return _container.ResolveAll(service).GetEnumerator(); } protected override void Configure() { _container = new WindsorContainer(); _container.AddFacility<ArrayDependencyFacility>(); GetCaliburnRegistrations().ForEach(x => _container.Register(x)); //GetPersistenceRegistrations().ForEach(x => _container.Register(x)); GetModuleRegistrations().ForEach(x => _container.Register(x)); GetShellRegistrations().ForEach(x => _container.Register(x)); _container.Install(_container.ResolveAll<IWindsorInstaller>()); LogManager.GetLog = type => new CaliburnLogger(type); base.Configure(); } } }

Es werden also alle Caliburn-relevanten Komponenten registriert, später alle Persistenz-relevanten Komponenten, die einzelnen Module, sowie die Shell selbst. Also, der Reihe nach:

Die Shell:

static IEnumerable<IRegistration> GetShellRegistrations()
{
   yield return Component
      .For<IShell>()
      .ImplementedBy<ShellViewModel>();
}

Caliburn-Komponenten Window-Manager und Event-Aggregator (ob man den Kernel selbst dazuzählen mag, ist Geschmackssache):

IEnumerable<IRegistration> GetCaliburnRegistrations()
{
    yield return Component
        .For<IWindowManager>()
        .ImplementedBy<WindowManager>();
    yield return Component
       .For<IEventAggregator>()
       .ImplementedBy<EventAggregator>();
    yield return Component
       .For<IWindsorContainer>()
       .Instance(_container);
}

Und letzten Endes die einzelnen Module des Office. Diese können (theoretisch) in der ausgeführten Assembly liegen, oder in einer der DLLs des aktuellen Verzeichnisses. Zusätzlich werden in diesen DLLs noch zusätzliche Windsor-Installer gesucht, falls für die nachgeladenen Module noch zusätzliche Kernel-Registrierungen vorzunehmen sind:

static IEnumerable<IRegistration> GetModuleRegistrations()
{
   yield return AllTypes
      .FromAssembly(Assembly.GetExecutingAssembly())
      .BasedOn<IModule>()
      .WithService.FromInterface(typeof(IModule));

   var directoryPath = AppDomain.CurrentDomain.BaseDirectory;
   foreach (var dllPath in Directory.GetFiles(directoryPath, "*.dll"))
   {
      Assembly assembly;
      try
      {
         assembly = Assembly.LoadFrom(dllPath);
         AssemblySource.Instance.Add(assembly);
      }
      catch (BadImageFormatException ex)
      {
         System.Diagnostics.Trace.TraceError(ex.ToString());
         continue;
      }
      catch (FileNotFoundException ex)
      {
         System.Diagnostics.Trace.TraceError(ex.ToString());
         continue;
      }
      yield return AllTypes
         .FromAssembly(assembly)
         .BasedOn<IModule>()
         .WithService.FromInterface(typeof(IModule));
      yield return AllTypes
         .FromAssembly(assembly)
         .BasedOn<IWindsorInstaller>()
         .WithService.FromInterface(typeof(IWindsorInstaller));
   }
}

Im wesentlichen war das der Bootstrapper, mit einer kleinen Ausnahme: es wurde noch eine ArrayDependencyFacility registriert, die bislang noch unresolved ist. Der Hintergrund ist der, daß man von Castle Windsor gerne ein komplettes Array aufgelöst bekommen möchte, hierfür aber eine eigene Facility registriert werden muß. Diese ist wie folgt implementiert:

using Castle.MicroKernel.Facilities;

namespace Lucifer.Office.Container
{
    public class ArrayDependencyFacility : AbstractFacility
    {
        protected override void Init()
        {
            Kernel.Resolver.AddSubResolver(new ArraySubDependencyResolver(Kernel));
        }
    }
}

was ziemlich unspektakulär ist, da an eine weitere unbekannte Klasse delegiert wird, die wie folgt aussieht:

using Castle.Core; using Castle.MicroKernel; using Castle.MicroKernel.Context; namespace Lucifer.Office.Container { public class ArraySubDependencyResolver : ISubDependencyResolver { readonly IKernel _kernel; public ArraySubDependencyResolver(IKernel kernel) { _kernel = kernel; } public object Resolve(CreationContext context, ISubDependencyResolver contextHandlerResolver

, ComponentModel model, DependencyModel dependency) { return _kernel.ResolveAll(dependency.TargetType.GetElementType(), null); } public bool CanResolve(CreationContext context, ISubDependencyResolver contextHandlerResolver

, ComponentModel model, DependencyModel dependency) { return dependency.TargetType != null && dependency.TargetType.IsArray && dependency.TargetType.GetElementType().IsInterface && !model.Parameters.Contains(dependency.DependencyKey); } } }

Damit ist der Bootstrapper abgearbeitet, es folgt etwas Sichtbares.

 

Die ShellView

Die ShellView soll einfach nur “gut” aussehen, mit einem Applikationsnamen, einem “sinngebenden” Icon, der Auswahlspalte links und dem Platzhalter des aktiven Moduls. Und damit das Fenster auch ein Icon bekommt, deklarieren wir ein Window:

<Window x:Class="Lucifer.Office.View.ShellView"
             Icon="..\Lucifer.Office.ico"
             Style="{DynamicResource ShellViewSkin}">

    <DockPanel Margin="20" LastChildFill="True">

        <StackPanel Style="{DynamicResource AppTitleStackPanelSkin}" DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,0,0,20">
            <Rectangle x:Name="rectangle" Height="100" Width="100" Margin="0" HorizontalAlignment="Left" Opacity="0.35" 
                       ToolTipService.ToolTip="{x:Static Resources:Strings.AppToolTip}">
                <Rectangle.Fill>
                    <ImageBrush Stretch="Uniform" ImageSource="/Lucifer.Office;component/Resources/Lucifer.Office.png" />
                </Rectangle.Fill>
            </Rectangle>
            <TextBlock x:Name="DisplayName" TextWrapping="Wrap" VerticalAlignment="Center" FontSize="48" Opacity="0.345"
                       Text="Lucifer Office">
                <TextBlock.Effect>
                    <DropShadowEffect BlurRadius="1" Direction="343" ShadowDepth="2" />
                </TextBlock.Effect>
            </TextBlock>
        </StackPanel>

        <ListBox x:Name="Items" Style="{DynamicResource AppNavigationItemSkin}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Button Margin="8" ToolTip="{Binding ToolTip}" HorizontalAlignment="Center" Width="140" cal:Message.Attach="ActivateItem($datacontext)">
                        <StackPanel>
                            <Image Source="{Binding IconFileName}" HorizontalAlignment="Center" Width="48" Height="48" />
                            <TextBlock Text="{Binding ModuleName}" HorizontalAlignment="Center" />
                        </StackPanel>
                    </Button>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ListBox>
        
        <ContentControl x:Name="ActiveItem" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" />
        
    </DockPanel>
</Window>

Das ShellView-Model

Die Aufgabe des Shellview-Models ist es, alle gefundenen Module zu sammeln und klickbar zu machen. Für den Rest sorgt Caliburn.Micro, insbesondere durch die Bindung des Listbox-Buttons mit der CM-Nachricht ActivateItem in obiger XAML-Datei. Gleichzeitig sorgt die Ableitung von Conductor<IScreen>.Collection.OneCtive, daß jeweils nur eine View aktiv ist:

using System.Collections.Generic; using System.Linq; using Caliburn.Micro; using Castle.Windsor; using Lucifer.Caliburn; using Lucifer.Office.Resources; namespace Lucifer.Office.ViewModel { public class ShellViewModel : Conductor<IScreen>.Collection.OneActive, IShell { readonly IWindsorContainer _container; IEnumerable<IModule> _modules; public IEnumerable<IModule> Modules

{ get { return _modules ?? (_modules = _container.ResolveAll<IModule>()); } } public ShellViewModel(IWindsorContainer container) { _container = container; } protected override void OnInitialize() { base.OnInitialize(); DisplayName = Strings.AppTitle; Items.AddRange(Modules); ActivateItem(Items.FirstOrDefault()); } } }

Ob man sich an dieser Stelle den Windsor-Container über den Konstruktor hineinreichen läßt, oder ihn durch IoC.Get ermittelt… es geht beides.

 

Die App.xaml

Wie bereits erwähnt, werden hier nur die Resource-Dictionaries aufgeführt, sowie der Bootstrapper bekanntgemacht. Wichtig ist noch, das von Visual Studio eingefügte Startfenster zu löschen:

<Application x:Class="Lucifer.Office.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
             xmlns:local="clr-namespace:Lucifer.Office">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="/PresentationFramework.Aero;V4.0.0.0;31bf3856ad364e35;component\themes/aero.normalcolor.xaml" />
                <ResourceDictionary Source="/Lucifer.Theme;component/Colors.xaml" />
                <ResourceDictionary Source="/Lucifer.Theme;component/Button.xaml" />
                <ResourceDictionary Source="/Lucifer.Theme;component/General.xaml" />
                <ResourceDictionary Source="/Lucifer.Theme;component/MainApp.xaml" />
                <ResourceDictionary Source="/Lucifer.Theme;component/IcsModule.xaml" />
                <ResourceDictionary Source="/Lucifer.Theme;component/PmsModule.xaml" />
                <ResourceDictionary Source="/Lucifer.Theme;component/UmsModule.xaml" />
                <ResourceDictionary>
                    <local:CastleBootstrapper x:Key="bootstrapper" />
                </ResourceDictionary>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

 

Lucifer.Theme

Auf das Theme selbst braucht an dieser Stelle nicht näher eingegangen zu werden (siehe Screenshot oben, auch “entwicklerschön” genannt). Geneigte Leser mögen sich die entsprechenden Stellen auf Github anschauen.

 

Ausblick

So ist in wenigen Schritten eine (leere) Hülle entstanden, die man zu allen möglichen Projekten ausbauen kann. Da an die Module keinerlei Anforderungen gestellt werden bis auf die Angabe der zur Darstellung benötigen Informationen, sind der Phantasie keinerlei Grenzen gesetzt.

Im nächsten Schritt wird der Zugriff auf die Daten erfolgen, um letztendlich über Listen und Dialoge die Bearbeitung zu ermöglichen.

DotNetKicks-DE Image
Advertisements
Veröffentlicht unter .NET, C#, Caliburn.Micro, WPF | 2 Kommentare

Log4net

1. Prolog

Der nachfolgende Text umfaßt einen Schnelleinstieg in die Konfiguration von Log4net. Das Beispiel-Projekt steht auf Github zur Verfügung.

1.1 Log4net bereitstellen

Per NuGet steht unter „Add library package reference“ das Paket log4net bereit. Alternativ kann man unter http://logging.apache.org/log4net/download.html ein entsprechendes Paket herunterladen.

1.2 Programm vorbereiten

Es gibt zwei Möglichkeiten, ein Programm zur Zusammenarbeit mit Log4net zu bewegen: entweder per Eintrag in der AssemblyInfo.cs oder programmatisch.

1.2.1 Per AssemblyInfo

In der Datei AssemblyInfo.cs ist die Zeile einzufügen:

[assembly: log4net.Config.XmlConfigurator()]

Falls es sich um ein Web-Projekt handelt, oder man aus einem anderen Grund die Konfigurationsdatei ändern möchte, kann man per optionalem Parameter eine andere
Konfigurationsdatei angeben:

[assembly: log4net.Config.XmlConfigurator(ConfigFile = "App.config")]

Ein weiterer Parameter namens Watch gibt an, ob die Konfigurationsdatei auf Änderungen hin überwacht werden soll:

[assembly: log4net.Config.XmlConfigurator(Watch=true)]
1.2.2 Programmatisch

Für das einfache Konfigurieren von Log4net genügt die Zeile

log4net.Config.XmlConfigurator.Configure();

Das Ändern der Konfigurationsdatei kann zB erfolgen durch

log4net.Config.XmlConfigurator.Configure(new FileInfo("log4net.config"));

Das Überwachen der Konfigurationsdatei wird erreicht mittels

log4net.Config.XmlConfigurator.ConfigureAndWatch(new FileInfo("log4net.config"));

in diesem Fall muss allerdings zwingend eine Konfigurationsdatei angegeben werden.

2. Logs ausgeben

Zur Ausgabe von Logs genügt es, vom LogManager von Log4net ein Handle für die aktuelle Klasse anzufordern und die Nachricht abzusetzen:

class Foo
{
  static readonly ILog Log = LogManager.GetLogger(typeof(Foo));
  public Foo()
  {
    Log.Info("c'tor of Foo");
  }
}

Je nach globalen Konfiguration der Logausgabe von Log4net und eventuell  vorgenommenen gesonderten Einstellungen der Klasse, in diesem Falle Foo, wird die Nachricht ausgegeben oder unterdrückt.
Zu den einfachen Textausgabe-Funktionen – Debug, Info, Warn, Error – gibt es eine korrespondierende Funktion zur formatierten Ausgabe von Text – DebugFormat, InfoFormat, WarnFormat, ErrorFormat:

Log.InfoFormat("Foo called at {0}", DateTime.Now);

3. Logs konfigurieren

Nachdem wir das Programm auf das Loggen vorbereitet und sogar schon erste Log-Ausgaben in die Klassen eingefügt haben, erhalten wir vorerst keine Ausgaben – es ist kein Log-Mechanismus konfiguriert, der die Ausgabe vornimmt.
Um wenigstens zu sehen, was eventuell schief läuft bei der Konfiguration von Log4net, können wir innerhalb der App.config (oder der jeweils angegebenen Konfigurationsdatei für Log4net) das interne Debugging anschalten:

<configuration>
  <appSettings>
    <add key="log4net.Internal.Debug" value="true" />
  </appSettings>
</configuration>

3.1 Globale Einstellungen

Die Konfiguration der Log-Mechanismen erfolgt mittels eines SectionHandlers von Log4net. Wir legen eine neue Sektion an und setzen den Default-Level erst einmal auf  Warn:

<configuration>
  <configSections>
    <section name="log4net"
      type="log4net.Config.Log4NetConfigurationSectionHandler, log4net"/>
  </configSections>
  <log4net>
    <root>
      <level value="WARN"/>
    </root>
  </log4net>
</configuration>

Damit ist erst einmal sichergestellt, daß Log4net konfiguriert wird und alle Nachrichten mit den Stufen Warn oder Error ausgegeben werden. Aber wohin?

3.1.1 Konsolenausgabe

Die erste Möglichkeit wäre die Ausgabe auf die Konsole. Hierfür müssen wir einen Appender einrichten und Log4net anweisen ihn zu benutzen:

...
<log4net>
 <root>
   <level value="WARN"/>
   <appender-ref ref="Console"/>
  </root>
 <appender name="Console" type="log4net.Appender.ConsoleAppender">
   <layout type="log4net.Layout.PatternLayout">
     <conversionPattern value="%date [%thread] %-5level %logger [%ndc] &lt;%property{auth}&gt; - %message%newline" />
   </layout>
 </appender>
/log4net>
...

Wichtig hierbei ist die Definition eines Ausgabe-Layouts, sonst beschwert sich Log4net. Alternativ kann auch das Layout auch mittels

<layout type="log4net.Layout.SimpleLayout" />

erfolgen. Ab jetzt sollte eine Ausgabe der Log-Nachrichten in der Konsole erscheinen! Wohlgemerkt auf der Konsole, nicht im Debug-Fenster von Visual Studio.

3.1.2 Farbige Konsolenausgabe

Wer es lieber bunt mag, kann auch die kolorierte Ausgabe des Konsolenappenders benutzen. Allerdings muß hierfür auch das Farbmapping angegeben werden:

...
<log4net>
  <root>
    <level value="WARN"/>
    <appender-ref ref="Colored"/>
  </root>
  <appender name="Colored" type="log4net.Appender.ColoredConsoleAppender">
    <mapping>
      <level value="FATAL" />
      <foreColor value="Red" />
      <backColor value="White" />
    </mapping>
    <mapping>
      <level value="ERROR" />
      <foreColor value="Red, HighIntensity" />
    </mapping>
    <mapping>
      <level value="WARN" />
      <foreColor value="Yellow" />
    </mapping>
    <mapping>
      <level value="INFO" />
      <foreColor value="Cyan" />
    </mapping>
    <mapping>
      <level value="DEBUG" />
      <foreColor value="Green" />
    </mapping>
    <layout type="log4net.Layout.SimpleLayout" />
    </appender>
</log4net>
...
3.1.3 Ausgabe im Debug-Fenster

Wie erwähnt, erscheint die Konsolenausgabe nicht im Debug-Fenster von Visual Studio. Hierfür existiert ein eigener Appender:

...
<log4net>
  <root>
    <level value="WARN"/>
    <appender-ref ref="Debug"/>
  </root>
  <appender name="Debug" type="log4net.Appender.DebugAppender">
    <immediateFlush value="true" />
    <layout type="log4net.Layout.SimpleLayout" />
  </appender>
</log4net>
...
3.1.4 Ausgabe in Datei

Die Ausgabe in eine Datei kann ganz einfach erfolgen über:

...
<log4net>
  <root>
    <level value="WARN"/>
    <appender-ref ref="File"/>
  </root>
  <appender name="File" type="log4net.Appender.FileAppender">
    <file value="log/myfile.txt" />
    <appendToFile value="true" />
    <encoding value="utf-8" />
    <layout type="log4net.Layout.SimpleLayout" />
  </appender>
</log4net>
...

Das Unterverzeichnis ‘log’ wird hierbei angelegt falls es nicht existiert.
Soll die Log-Datei hierbei eine gewisse Größe nicht überschreiten, oder soll der Dateiname in Abhängigkeit des Datums gebildet werden, empfiehlt sich der Einsatz des RollingFileAppenders:

...
<log4net>
  <root>
    <level value="WARN"/>
    <appender-ref ref="RollSize"/>
    <appender-ref ref="RollDate"/>
  </root>
  <appender name="RollSize" type="log4net.Appender.RollingFileAppender">
    <file value="log/SizeLog.txt" />
    <appendToFile value="true" />
    <rollingStyle value="size" />
    <maxSizeRollBackups value="10" />
    <maximumFileSize value="1MB" />
    <layout type="log4net.Layout.SimpleLayout" />
  </appender>
  <appender name="RollDate" type="log4net.Appender.RollingFileAppender">
    <file value="log/DateLog.txt" />
    <appendToFile value="true" />
    <rollingStyle value="date" />
    <layout type="log4net.Layout.SimpleLayout" />
  </appender>
</log4net>
...

4. Ausgabe anpassen

Wie in einem Beispiel bereits gesehen, kann die Log-Ausgabe auch an persönliche Bedürfnisse angepasst werden. Hierfür wird das Layout nicht auf SimpleLayout gesetzt, sondern explizit per Patternlayout angegeben:

...
<appender name="Console" type="log4net.Appender.ConsoleAppender">
  <layout type="log4net.Layout.PatternLayout">
  <conversionPattern value="%date [%thread] %-5level %logger [%ndc] &lt;%property{auth}&gt; - %message%newline" />
  </layout>
</appender>
...

Um die Ausgabe anzupassen, stehen folgende Kürzel bereit:

Pattern Kürzel Bedeutung
appdomain a
class c Äquivalent zu type
date d Datum
exception Gespeicherte Exception, leer wenn keine verfügbar
file f Dateiname des Log-Aufrufs (*)
identity Benutzername (*)
location l FQN des Log-Aufrufs (*)
level p Level des Log-Events
line L Zeilennummer des Log-Aufrufs (*)
logger Klassenname des Log-Aufrufers
message m Die Log-Nachricht
method M Der Methodenname des Log-Aufrufs (*)
newline n Beginnt eine neue Zeile
ndc Ausgabe des Nested Diagnostic Context
property P Ausgabe einer Log-Event-Property, zB
%property{User}
timestamp r Millisekunden seit Applikationsstart
thread t Thread-ID sofern verfügbar
type C FQN des Typs des Log-Aufrufers (*)
username w Name des aktuellen Windows-Benutzers (*)
utcdate Ausgabe des Datums im UTC-Format

Quelle: http://logging.apache.org/log4net/release/sdk/log4net.Layout.PatternLayout.html

Warnung: Die mit (*) gekennzeichneten Pattern verlangsamen den Log-Mechanismus extrem und sollten vermieden werden.

Datums-/Uhrzeitangaben können in gewohnter Weise mit den üblichen Kürzeln genauer spezifiziert werden, zB mit %utcdate{dd MMM yyyy HH:mm:ss,fff}

Weiterhin können die Einträge auch auf bestimmte Längen formatiert werden mit der Anweisung %[-][min].[max][eintrag]
Hier bedeutet – rechtsbündiges Auffüllen, auf die min-Werte wird aufgefüllt, ab den max-Werten wird abgeschnitten, zB %-20.30logger oder %.10logger.

5. Log pro Klasse

Das global eingestellte Log-Verhalten kann pro Klasse überschrieben werden. Damit ist es möglich, nur bestimmte Klassen bzw deren Methoden zu protokollieren, oder auch nur diese Log-Nachrichten auf anderen Appendern auszugeben.
Hierzu wird für diese Klasse eine eigene Einstellungsebene hinzugefügt:

...
<root>
  <level value="WARN"/>
  <appender-ref ref="Console"/>
</root>
<logger name="HelloLog.Foo">
  <level value="ALL" />
  <appender-ref ref="Colored"/>
</logger>
...

In diesem Beispiel werden alle Log-Nachrichten der Stufe Warn auf der Konsole ausgegeben, für die Klasse HelloLog.Foo allerdings werden alle Nachrichten ausgegeben, zusätzlich werden diese Nachrichten noch farbig auf der Konsole ausgegeben.

Dieses Überschreiben kann auch per Namensraum geschehen. Mit:

...
<logger name="HelloLog">
  <level value="ALL" />
  <appender-ref ref="Colored"/>
</logger>
...

gilt oben beschriebenes Verhalten für alle Klassen im Namensraum HelloLog.

 

6. Epilog

Es existieren noch weit mehr Appender als hier angegeben, zB Ausgabe auf Twitter, MongoDB, etc…
Hier noch eine kleine Übersicht der mitgelieferten Appender:

log4net.Appender. Beschreibung
AdoNetAppender Ausgabe in Datenbank
AnsiColorTerminalAppender Ausgabe auf Terminal mittels ANSI-Farbsequenzen
AspNetTraceAppender Ausgabe in ASP.NET Trace-Kontext
BufferingForwardingAppender Puffert Einträge und leitet sie weiter
ColoredConsoleAppender Farbige Ausgabe in Konsole
ConsoleAppender Ausgabe in Konsole
DebugAppender Ausgabe in das Debug-Fenster
EventLogAppender Ausgabe in das Event-Log
ForwardingAppender Weiterleitung der Einträge
FileAppender Ausgabe in Datei
LocalSyslogAppender Ausgabe in das Syslog-System
MemoryAppender
NetSendAppender
OutputDebugStringAppender
RemoteSyslogAppender
RemotingAppender
RollingFileAppender Ausgabe in Datei mit rotierendem Dateinamen
SmtpAppender Versenden der Einträge per Mail
SmtpPickupDirAppender Versenden der Einträge per Mail-Programm
TelnetAppender Ausgabe auf Telnet-Host, mit dem man sich verbinden kann
TraceAppender Ausgabe in das Trace-System
UdpAppender Versenden der Log-Nachrichten per UDP

Quelle: http://www.beefycode.com/post/Log4Net-Tutorial-pt-3-Appenders.aspx

Hinweis: es wurden beileibe nicht alle Möglichkeiten von Log4net aufgezeigt. ZB bestehen diverse Filter-Möglichkeiten bei der Weiterleitung der Log-Events. Weiterführende Informationen finden sich auf der Seite http://logging.apache.org/log4net/release/sdk/

DotNetKicks-DE Image
Veröffentlicht unter .NET, C#, Logging | Kommentar hinterlassen

Automatische Updates

Prolog

Gewünscht wäre ein automatisches Update-System à la Debian apt. Nun ist nuget schon ein Schritt in die richtige Richtung, allerdings mit seiner Visual-Studio-Integration mehr für Entwickler gedacht. An dieser Stelle kommt NetSparkle ins Spiel. Laut Beschreibung stellt es einen automatischen Update-Mechanismus für einzelne Programme zur Verfügung.

Erster Anlauf

Nach dem Erstellen einer simplen Forms-Anwendung und dem Hinzufügen einer Referenz auf AppLimit.NetSparkle.Net40 muss der Update-Mechanismus initialisiert werden. Hierfür genügen laut Dokumentation die Zeilen

 public partial class MainForm : Form
{
  private Sparkle _sparkle;

  public MainForm()
  {
    InitializeComponent();

    _sparkle = new Sparkle("http://www.slesa.de/download/updates/HelloSparkle/versioninfo.xml");
    _sparkle.StartLoop(true);
  }
}

Ein erster Probelauf im Debugger offenbart, dass noch die Company-Information fehlt. Bei der Gelegenheit kann auch gerade die Versionsnummer in der AssemblyInfo.cs angepasst werden. Für einen ersten Anlauf erscheint Version 1.0.0 zu hoch gegriffen:

[assembly: AssemblyCompany("Slesa Inc.")]
[assembly: AssemblyVersion("0.0.1.0")]
[assembly: AssemblyFileVersion("0.0.1.0")]

Startet man die Applikation erneut, läuft sie und tut offenbar nichts. Dabei pollt sie im Hintergrund bereits auf eine Versionsinfo, die auf eine neue Version hindeutet. Legen wir also die versioninfo.xml an

<?xml version="1.0" encoding="utf-8"?>

<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle"  xmlns:dc="http://purl.org/dc/elements/1.1/">
    <channel>
        <title>HelloSparkle Update</title>
        <link>http://www.slesa.de/download/updates/HelloSparkle/versioninfo.xml</link>
        <description></description>
        <language>en</language>
        <item>
            <title>Version 1.0.1</title>
            <sparkle:releaseNotesLink>http://update.applimit.com/netsparkle/1.0.1/rnotes.html</sparkle:releaseNotesLink>
            <pubDate>Sun, 31 Oct 2010 10:21:11 +0000</pubDate>
            <enclosure
                url="http://update.applimit.com/netsparkle/1.0.1/NetSparklTestApp-1.0.1.exe"
                length="5120"
                type="application/octet-stream"
                sparkle:version="1.0.1"
                sparkle:dsaSignature="ngnGYS1ELmGWlhWp0FAzwjmc8VdAFkyIuoybSUwPkpmt+HusArTINQ=="
            />
        </item>
    </channel>
</rss>

und kopieren die Datei auf unseren Server. Jetzt sollte ein Fenster erscheinen, in dem auf die neue Version hingewiesen wird.

HelloSparkle

Fehlersuche

Nach dem Beenden und erneuten Starts des Programms erscheint kein weiterer Hinweis. NetSparkle scheint, kaum gelaufen, schon kaputt zu sein. Wir lassen uns mal ein Diagnose-Fenster anzeigen:

  _sparkle = new Sparkl("http://www.slesa.de/download/updates/HelloSparkle/versioninfo.xml");
  _sparkle.ShowDiagnosticWindow = true;
  _sparkle.StartLoop(true);

Wichtig ist, ShowDiagnosticWindow vor dem Start des Loops zu setzen.

HelloSparkleDiagnostics

Schon sehen wir, warum keine weiteren Fenster erscheinen: Update check performed within the last 24 hours! Ein kurzes Stöbern in der Registry offenbart: NetSparkle hat einen Schlüssel

HKEY_CURRENT_USER\Software\Slesa Inc.\HelloSparkle\AutoUpdate

angelegt, unter dem die Daten des Software-Updates gespeichert werden. Nach dem Löschen des Eintrags LastTimeCheck wird erneut auf ein Update geprüft.

Sicherheit

Da NetSparkle Programme aus dem Internet herunterlädt und diese auf dem lokalen Rechner ausführt, sollte man die Update-Pakete absichern. Hierfür wird mit den mitgelieferten Tools zuerst ein Schlüsselpaar generiert:

NetSparkleDSAHelper.exe /genkey_pair

erzeugt die Dateien NetSparkle_DSA.priv und NetSparkle_DSA.pub. Letztere – der öffentliche Schlüssel – wird dem Projekt hinzugefügt und als Embedded Resource markiert:

HelloSparklePubKey

Nach erneutem Starten (nachdem man wiederum den Registry-Schlüssel gelöscht hat) erscheint nun ein Hinweis, dass mit dem Update-Paket nicht alles so ist wie es sein sollte:

HelloSparkleWarning

Um diese Warnung zu umgehen, muss das Paket signiert werden, und zwar mit dem privaten Schlüssel, den wir gerade erzeugt haben:

NetSparkleDSAHelper.exe /sign_update [YourUpdate.Exe] NetSparkle_DSA.priv

Fazit

Mit wenigen Handgriffen hat man seine Applikation auf Versionsupdates vorbereitet. Durch Anpassen der Links innerhalb der versioninfo.xml kann man eigene Release-Notes anzeigen lassen, und natürlich sollte man auf das eigene Update-Programm verweisen. Spannend erscheint jetzt noch die Frage, ob das Erzeugen von entsprechenden Updatern mit Hilfe von WiX gelingt, oder ob es da weitere Hürden zu umschiffen gilt.

DotNetKicks-DE Image
Veröffentlicht unter .NET, C#, Updates | 3 Kommentare