# Wednesday, January 31, 2007

Im Moment bin ich ein bisschen irritiert, wie sich WPF unter die CultureInfo verhält.

Ich verwende ein englisches Windows XP und habe im Control Panel unter Regional Options die "Standards and formats" auf German gestellt. Unter Windows Forms hat das standardmäßig dazu geführt, dass z.B: das Datum deutsch formatiert wurde. Auch Office und andere Anwendungen respektieren diese Einstellung.

Jeder Thread hat unter anderem zwei Properties: CurrentCulture und CurrentUICulture.

CurrentCulture: Das ist die Eigenschaft die ich im Control Panel eingestellt habe und bestimmt, wie Ausgaben formatiert werden. Bei mir "de-DE".

CurrentUICulture: Das ist die Sprache des Betriebssystems und bestimmt welche Elemente aus den Ressourcen verwendet werden. Quasi die Übersetzung. Bei mir "en-US".

Unter WPF kriege ich aber bisher nur die CurrentUICulture zu sehen. In meinem Fall eben "en-US". Folglich ist auch das Datum falsch formatiert. Ich möchte, dass die Einstellungen im Control Panel honoriert werden. Also basteln wir einen DateTimeConverter:

using System;
using System.Windows.Data;
using System.Globalization;
using System.Threading;

namespace CPTec.SPCat.SPCatClient
{
    /// <summary>
    /// Kurze Datums+Zeit Darstellung. Verwendet CurrentCulture und nicht CurrentUICulture
    /// </summary>
    [ValueConversion(typeof(DateTime), typeof(String))]
    public class DateTimeConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            DateTime date = (DateTime)value;
            return date.ToString("g", Thread.CurrentThread.CurrentCulture);
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            string strValue = value.ToString();
            DateTime resultDateTime;
            if (DateTime.TryParse(strValue, Thread.CurrentThread.CurrentCulture, DateTimeStyles.None, out resultDateTime))
            {
                return resultDateTime;
            }
            return value;
        }
    }
}

Die Klasse implementiert IValueConverter einem neuen Interface in .NET 3.0 das beim Data Binding die Konvertierung von dargestelltem Wert und gebundenem Wert in beide Richtungen erlaubt.

Das Datum wird gezielt mit dem IFormatProvider aus CurrentCulture und nicht CurrentUICulture formatiert.

Der DateTimeConverter soll in der ganzen Applikation zur Verfügung stehen, also kommt er zu den Application.Resources in App.xaml:

<Application x:Class="CPTec.SPCat.SPCatClient.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:test="clr-namespace:CPTec.SPCat.SPCatClient" 

    Startup="AppStartup"
    >
    <Application.Resources>
      <test:DateTimeConverter x:Key="dateTimeConverter" />
    </Application.Resources>
</Application>

Und so verwende ich diese Resource dann beispielsweise in einer GridViewColumn:

  <GridViewColumn Header="Last Upload" 
    DisplayMemberBinding="{Binding Path=LastUpload,
      Converter={StaticResource dateTimeConverter}}" />

Dabei ist LastUpload vom Typ DateTime.

Obwohl das sicher eine Lösung für das Problem ist, frage ich mich schon, wieso das sinnvolle Konzept der getrennten Einstellung von Sprache und Formatierung wieder verschwunden ist? Oder zumindest nicht standardmäßig so wie bisher funktioniert. Oder übersehe ich etwas?

Wednesday, January 31, 2007 5:38:34 PM (W. Europe Standard Time, UTC+01:00)  #
  Disclaimer  |  Comments [0]  | 

Hurra, auch dieses Blog verbrennt jetzt seine Einspeisung. Sprich ich nutze die Dienste von FeedBurner.

Der neue FeedBurner Feed steht unter: http://feeds.feedburner.com/cdeger zur Verfügung.

Die auffälligste Änderung ist das FeedFlare, das jetzt unter jedem Eintrag zu sehen ist. Mit all den wunderbaren, neuen sozialen Netzwerken :).

Wednesday, January 31, 2007 12:06:23 PM (W. Europe Standard Time, UTC+01:00)  #
  Disclaimer  |  Comments [0]  | 
# Friday, January 26, 2007

Die Benutzeroberfläche soll auf den Anwender reagieren, obwohl gerade über HTTP Daten von einem SQL Server geladen werden. Also wird diese Arbeit über einen Background Thread erledigt. Der Fortschritt und das Ende dieser Aufgabe soll natürlich dem Anwender dargestellt werden. Nur darf dieser neue Thread nicht auf die Elemente der Benutzeroberfläche zugreifen. Die gehört dem Thread, der sie erzeugt hat. Andere Threads dürfen da nicht ran. Das gilt sowohl für Windows Forms als auch für die Windows Presentation Foundation.

In Windows Forms haben dazu die Controls ein Property InvokeRequired, das angibt, ob man mit Invoke oder BeginInvoke in den UI Thread Kontext wechseln muss. Um das ganze zu Kapseln habe ich den Background Threads immer eine Referenz auf ein Control mitgegeben. Damit konnte die Callbacks wieder auf dem UI Thread stattfinden. Ist aufwendig und man hat in der Business Logik Referenzen auf UI Elemente.

Mit .NET 3.0 gibt es für WPF den Dispatcher und ein DispatcherObject. Damit wird die Kontrolle über den Thread Kontext vereinfacht. Man leitet seine Hintergrund-Arbeiter von DispatcherObject ab und kriegt die Möglichkeit für UI Thread Aufrufe quasi geschenkt.

Und jetzt das Angenehme: Dieser neue Dispatcher funktioniert mit Windows Forms auch. Es wird also nicht nur einfacher, sondern man muss nicht mit unterschiedlichen Methoden für jede UI Technologie arbeiten.

Friday, January 26, 2007 10:37:18 AM (W. Europe Standard Time, UTC+01:00)  #
  Disclaimer  |  Comments [0]  | 
# Wednesday, January 24, 2007

Mit System.Windows.Forms.Integration kann man Windows Forms Controls in WPF Fenstern und umgekehrt darstellen. Ich möchte aber klassische Windows Forms Fenster und neue WPF Fenster parallel darstellen. Das heißt ich mische auf Fenster und nicht auf der darunter liegenden Control Ebene.

Ein einem WPF Projekt kann man ganz einfach auch eine Windows Forms anzeigen. Man muss nur eine Referenz auf System.Windows.Forms hinzufügen und kann wie bisher Formen erzeugen:

Form1 form = new Form1()
form.Show()

Die so erzeugte Windows Form funktioniert offensichtlich problemlos. Nach einigen Tests ist mir aber ein irritierendes Verhalten aufgefallen: Beim Login setze System.Threading.Thread.CurrentPrincipal auf einen eigenen IPrincipal. Nach einem Klick auf einen Button in der Windows Form ist der CurrentPrincipal aber wieder auf GenericPrincipal gesetzt. Der Thread ist aber der gleiche. Irgendwie ist in der Message Loop dieser Event anders aufgerufen wurden, als in meiner reinen Windows Form Applikation davor.

Nach einigem Debuggen und Stirnrunzeln habe ich eine Lösung gefunden. In der Windows Form Applikation wurde der Message Loop mit

System.Windows.Forms.Application.Run(new LaunchForm());

gestartet. Die WPF Applikation startet aber über System.Windows.Application. Soweit ich das verstehe, kriegen die Windows Forms davon nichts mit. eine Lösung scheint darin zu bestehen, als letzten Befehl im System.Windows.Application.Startup Event noch eine Windows Forms Applikation zu starten

System.Windows.Forms.Application.Run();

Dieser Workaround scheint zu funktionieren. Der Thread.CurrentPrincipal ist auch nach Windows Forms Messages richtig gesetzt. Ich hoffe, dass ich mir damit keine anderen, unerwünschten Nebeneffekte einhandle.

Wednesday, January 24, 2007 2:54:59 PM (W. Europe Standard Time, UTC+01:00)  #
  Disclaimer  |  Comments [0]  | 
# Tuesday, January 23, 2007

Irgendetwas verstehe ich am ObjectDataProvider nicht oder das Design hat eine Schwäche.

Im ersten Versuch habe ich den ObjectDataProvider in XAML definiert und über IsInitialLoadEnabled="False" wollte ich verhindern, dass der ObjectDataProvider bei der Initialisierung bereits Daten zieht. Der ObjectDataProvider soll seine Daten über eine statische Methode mit einem Parameter beziehen. Den Objekt Typ, den Methodennamen und einen Null-Paramater hatte ich in XAML definiert. Erst beim Aufruf einer Methode im Code-behind sollte die Daten erstmalig gelesen werden. Vorher ist der Parameter noch nicht bekannt.

Das hat auch geklappt. Aber nur mit einem Fehler im Debug-Output. Über Reflection konnte der ObjectDataProvider keine passende Signatur in angegeben Klasse finden. Klar, der Parameter ist ja auch nicht vom Typ System.Object, sondern ein eigener Typ BinaryHash. Auf den ersten Blick sah das natürlich so aus, als ob gar nicht versucht wurde, Daten zu laden.

Aber wieso wurde versucht Daten zu laden, obwohl IsInitialLoadEnabled="False" gesetzt war? Soweit ich das verstehe, hilft dieser Parameter nichts, wenn man den ObjectDataProvider in XAML definiert.

Der ObjectDataProvider (vererbt über DataSourceProvider) implementiert ISupportInitialize und der XAML Loader ruft entsprechend BeginInit und EndInit auf. In EndInit vom DataSourceProvider wird über Umwege schließlich Refresh aufgerufen. IsInitialLoadedEnabled wird dabei gar nicht getestet.

Wenn ich das alles richtig verstehe, dann darf muss man in diesem Fall den ObjectDataProvider im Code erzeugen. Nur dann ist sicher gestellt, dass das Objekt erst Daten bezieht, wenn man sie wirklich benötigt.

Tuesday, January 23, 2007 6:19:38 PM (W. Europe Standard Time, UTC+01:00)  #
  Disclaimer  |  Comments [0]  | 

Update: Die Lösung erzeugt einen Fehler im Debug-Output. Mehr im nächsten Posting.

 

Die Windows Presentation Foundation (WPF) ist die neue Technologie von Microsoft für grafische Benutzeroberflächen. Mit .NET 3.0 steht diese Erweiterung unter Windows XP und Vista zur Verfügung.

Weil das Ganze aber noch sehr neu ist, sind weder die Tools, noch die Dokumentation oder Beispiele ausgereift. Im Netz findet man viel Code, der mit den Betas, aber nicht mit der Release Version funktioniert. Viele stürzen sich mit Begeisterung auf Animationen und die 3D Funktionen. Das alltägliche Basteln von eher normalen Dialogen ist noch die Ausnahme.

Ich in meinem aktuellen Projekt, wird ein Großteil der Dialoge noch mit der ausgereifteren Windows Forms Technologie erstellt. Aber an einigen Stellen möchte ich doch die Vorteile der WPF nutzen. Insbesondere der Umgang mit Grafiken soll darauf basieren. Auch in diesen Bereichen will man aber mal eine Liste oder ein Grid darstellen. Der Inhalt soll natürlich per Datenbindung aus den Business Objekten kommen.

Die erste Aufgabe war, eine kleine Liste mit Dateinamen und einem Datum darzustellen. Aus dem Business Objekt kriege ich diese Liste als BindingList<T>. In Windows Forms erzeugt man einfach eine ObjectDataSource zu den Detailinformationen (hier UploadedFilesNameDateInfo). Zieht eine BindingSource auf die Form. Setzt dessen DataSource auf die gerade erzeugte ObjectDataSource. Am DataGridView setzt man die DataSource auf diese BindingSource. Es erschienen die passenden Zellen im Grid und im Designer arrangiert man alles wie gewünscht. Im Exzerpt sieht das dann in *.Designer.cs etwa so aus:

this.uploadedFilesNameDateInfoBindingSource.DataSource = 
	typeof(CPTec.SPCat.Business.Blobs.UploadedFilesNameDateInfo);
this.dataGridView1.DataSource = this.uploadedFilesNameDateInfoBindingSource;

Die Daten sollen nicht beim Laden der Form gezogen werden, sondern erst zu einem späteren Zeitpunkt. Das Business Objekt braucht auch einen Parameter:

private void DisplayHashInfo(BinaryHash binaryHash)
{
    uploadedFilesNameDateInfoBindingSource.DataSource = 
        UploadedFilesNameDateList.GetUploadedFilesNameDateList(binaryHash));
}

In WPF ist die ganze Datenbindung wesentlich mächtiger geworden, dafür sind die einfachen Dinge zumindest am Anfang nicht so offensichtlich. Man kann einen ObjectDataProvider definieren und diesem eine Methode mit Parametern angeben, von dem er die Daten beziehen soll. Die ListView mit GridView wird an diesen DataProvider gebunden.

Fast alle Beispiele definieren diese Objekte über XAML. Damit werden alle Objekte beim Laden der Form initialisiert. Ich will aber erst später die Daten beziehen.
Alternative könnte man die Objekte im Code erzeugen. Damit hat man die Kontrolle über den Ablauf. Grundsätzlich ist aber XAML gerade für die Objekte-Erzeugung gedacht und deswegen möchte ich diesen Weg nicht bei der ersten Schwierigkeit verlassen.

Mit IsInitialLoadEnabled kann man dem ObjectDataProvider sagen, dass er ohne Daten erzeugt werden soll. Als ersten Parameter erhält er Null. Dieser Parameter wird dann per Code später gesetzt und der DataProvider zum Laden der Daten veranlasst.

XAML:

<Window x:Class="CPTec.SPCat.SPCatClient.Window2"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:blobs="clr-namespace:CPTec.SPCat.Business.Blobs;assembly=SPCat.Business"
    xmlns:system="clr-namespace:System;assembly=mscorlib" 
    Title="CPTec.SPCat.SPCatClient" Height="100" Width="300"
    >
  <Window.Resources>
    <ObjectDataProvider x:Key="odpUploadedFiles"
                     ObjectType="{x:Type blobs:UploadedFilesNameDateList}"
                     MethodName="GetUploadedFilesNameDateList"
                     IsInitialLoadEnabled="False" >
      <ObjectDataProvider.MethodParameters>
        <system:Object>null</system:Object>
      </ObjectDataProvider.MethodParameters>
    </ObjectDataProvider>
  </Window.Resources>

  <ListView Grid.Row="1" Name="UploadedFilesInfos"
            ItemsSource="{Binding Source={StaticResource odpUploadedFiles}}"
            MouseDoubleClick="DoubleClick" >
    <ListView.View>
      <GridView>
        <GridViewColumn DisplayMemberBinding="{Binding Path=Name}"
                        Header="Name" />
        <GridViewColumn DisplayMemberBinding="{Binding Path=LastUpload}"
                        Header="Last Upload" />
      </GridView>
    </ListView.View>
  </ListView>
</Window>

Code-behind:

using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Controls;
using System.Windows.Data;

using CPTec.SPCat.Business.Settings;
using CPTec.SPCat.Business.Blobs;

namespace CPTec.SPCat.SPCatClient
{
    public partial class Window2 : System.Windows.Window
    {
        public Window2()
        {
            InitializeComponent();
        }

        private void DoubleClick(object sender, MouseButtonEventArgs e)
        {
            SettingsHashList list = SettingsHashList.GetSettingsHashList();
            DisplayHashInfo(list[0].Value);
        }

        private void DisplayHashInfo(BinaryHash binaryHash)
        {
            ObjectDataProvider odb = 
                    (ObjectDataProvider)this.Resources["odpUploadedFiles"];
            odb.MethodParameters[0] = binaryHash;
            odb.Refresh();
        }
    }
}

Der MouseDoubleClick Event am ListView dient nur dazu, um das nachträgliche Laden der Liste zu verdeutlichen.

Das sieht ja schon mal ganz gut aus. Nur wieso ist das Datum nicht richtig formatiert? Ich denke es geht mit dem IValueConverter weiter...

Tuesday, January 23, 2007 11:43:41 AM (W. Europe Standard Time, UTC+01:00)  #
  Disclaimer  |  Comments [0]  | 
# Friday, January 12, 2007

In meinem aktuellen Projekt werden binäre Objekte, wie zum Beispiel Bitmap Grafiken im SQL Server unter dem Daten Typ varbinary(MAX) gespeichert. Der Zugriff auf die Objekte findet über deren Hash statt. Einige Objekte sollten bereits bei einer ganz neu erzeugten Datenbank vorhanden sein.

Eine Anforderung ist, dass die komplette Datenbank über SQL Skripte erstellt werden kann. Diese Skripte sind natürlich unter Source Control im Team Foundation Server. Aber wie kriege ich jetzt die binären Daten meiner Objekte in ein Skript? Mit den Bordmittel habe ich das nicht geschafft.

Meine Datenbank Diagramme sichere ich mit Hilfe der Skripte aus diesem sehr guten Artikel auf The Code Project: Script SQL Server 2005 diagrams to a file

Darin enthalten ist eine T-SQL User Defined Function mit dem Namen Tool_VarbinaryToVarcharHex. Das ist der wesentliche Baustein, um binäre Daten in einem Skript automatisiert darstellen zu können.

Wichtig ist noch, um den INSERT und UPDATE Befehl einen Try Block zu haben. Falls das Objekt schon existiert, würde der INSERT Befehl scheitern, aber alle nachfolgenden UPDATE Befehle würden die binären Daten anhängen. Das Objekt wäre dann doppelt so groß.

Das komplette Skript erzeugt alle Befehle, um den momentanen Inhalt der Tabelle anzulegen und die binären Daten häppchenweise hineinzuschreiben:

DECLARE @Hash binary(24)
DECLARE @HashText varchar(60)
DECLARE @ObjectData varbinary(MAX)
DECLARE @Line varchar(MAX)
DECLARE @size            INT
DECLARE @index            INT
DECLARE @chunk            INT

DECLARE blobs CURSOR FOR SELECT Hash,ObjectData FROM BinaryObjects WHERE Complete=1
OPEN blobs
FETCH NEXT FROM blobs INTO @Hash,@ObjectData
WHILE (@@FETCH_STATUS = 0)
BEGIN
    SET @HashText = ' 0x' + UPPER(dbo.Tool_VarbinaryToVarcharHex (@Hash))
    SET @size = DATALENGTH(@ObjectData)
    SET @index = 1
    SET @chunk = 32
    PRINT ''
    PRINT '-- BEGIN BLOB'
    PRINT 'BEGIN TRY'
    PRINT '  INSERT INTO BinaryObjects (Hash, ObjectData, Complete) VALUES'
    PRINT '    (' + @HashText + ', 0x, 0)'

    WHILE @index < @size
    BEGIN
        -- Die Objekt Daten häppchenweise ausgeben
        SELECT @line =  
             '    ('
            + '0x' + UPPER(dbo.Tool_VarbinaryToVarcharHex (SUBSTRING (ObjectData, @index, @chunk)))
            + ', null, 0)'
        FROM    BinaryObjects
        WHERE    Hash = @Hash
        PRINT '  UPDATE BinaryObjects SET ObjectData .Write'
        PRINT @line
        PRINT '    WHERE Hash = ' + @HashText
        SET @index = @index + @chunk
    END
    PRINT '  UPDATE BinaryObjects SET Complete = 1 '
    PRINT '    WHERE Hash =' + @HashText
    PRINT 'END TRY'
    PRINT 'BEGIN CATCH'
    PRINT '  PRINT ''Hash ' + @HashText + ' already exists'''
    PRINT 'END CATCH'
    PRINT '-- END BLOB'

    FETCH NEXT FROM blobs INTO @Hash,@ObjectData
END
CLOSE blobs
DEALLOCATE blobs

Dieses Skript erzeugt beispielsweise folgende Ausgabe:

 
-- BEGIN BLOB
BEGIN TRY
  INSERT INTO BinaryObjects (Hash, ObjectData, Complete) VALUES
    ( 0xA9199ED44700D84F80BBE5F6F2B2F045767C1E2300000132, 0x, 0)
  UPDATE BinaryObjects SET ObjectData .Write
    (0xFFD8FFE000104A46494600010200006400640000FFEC00114475636B79000100, null, 0)
    WHERE Hash =  0xA9199ED44700D84F80BBE5F6F2B2F045767C1E2300000132
  UPDATE BinaryObjects SET ObjectData .Write
    (0x04000000000000FFEE000E41646F62650064C000000001FFDB0084001B1A1A29, null, 0)
    WHERE Hash =  0xA9199ED44700D84F80BBE5F6F2B2F045767C1E2300000132
  UPDATE BinaryObjects SET ObjectData .Write
    (0x1D2941262641422F2F2F42473F3E3E3F47474747474747474747474747474747, null, 0)
    WHERE Hash =  0xA9199ED44700D84F80BBE5F6F2B2F045767C1E2300000132
  UPDATE BinaryObjects SET ObjectData .Write
    (0x47474747474747474747474747474747474747474747474747474747011D2929, null, 0)
    WHERE Hash =  0xA9199ED44700D84F80BBE5F6F2B2F045767C1E2300000132
  UPDATE BinaryObjects SET ObjectData .Write
    (0x3426343F28283F473F353F474747474747474747474747474747474747474747, null, 0)
    WHERE Hash =  0xA9199ED44700D84F80BBE5F6F2B2F045767C1E2300000132
  UPDATE BinaryObjects SET ObjectData .Write
    (0x4747474747474747474747474747474747474747474747474747474747FFC000, null, 0)
    WHERE Hash =  0xA9199ED44700D84F80BBE5F6F2B2F045767C1E2300000132
  UPDATE BinaryObjects SET ObjectData .Write
    (0x11080010001003012200021101031101FFC4004C000101000000000000000000, null, 0)
    WHERE Hash =  0xA9199ED44700D84F80BBE5F6F2B2F045767C1E2300000132
  UPDATE BinaryObjects SET ObjectData .Write
    (0x0000000000000401010100000000000000000000000000000406100100000000, null, 0)
    WHERE Hash =  0xA9199ED44700D84F80BBE5F6F2B2F045767C1E2300000132
  UPDATE BinaryObjects SET ObjectData .Write
    (0x000000000000000000000000110100000000000000000000000000000000FFDA, null, 0)
    WHERE Hash =  0xA9199ED44700D84F80BBE5F6F2B2F045767C1E2300000132
  UPDATE BinaryObjects SET ObjectData .Write
    (0x000C03010002110311003F008000D46FFFD9, null, 0)
    WHERE Hash =  0xA9199ED44700D84F80BBE5F6F2B2F045767C1E2300000132
  UPDATE BinaryObjects SET Complete = 1 
    WHERE Hash = 0xA9199ED44700D84F80BBE5F6F2B2F045767C1E2300000132
END TRY
BEGIN CATCH
  PRINT 'Hash  0xA9199ED44700D84F80BBE5F6F2B2F045767C1E2300000132 already exists'
END CATCH
-- END BLOB

Und was steckt da für ein binäres Objekt in dem Skript? Ganz klar, eine kleines, rotes Quadarat! Gespeichert mit 16x16 Pixel im Jpeg Format.

Friday, January 12, 2007 2:17:03 PM (W. Europe Standard Time, UTC+01:00)  #
  Disclaimer  |  Comments [0]  |