Archiv

Archiv für die Kategorie ‘Microsoft .NET’

Vererbung von Properties entfernt die Attribute

Für eine Erweiterung von meinem Quellcode hatte ich geplant, ein paar Properties mit Attributen zu versehen. Dabei sollten die Properties virtual sein. Die Abfrage der Attribute sollte dann mittels GetCustomAttributes geschehen. Leider hat das gar nicht funktioniert, was auch schon andere .NET-Programmierer feststellen mussten. Darum habe ich die Problemstellung extrahiert:

Zum Prüfen der Attribute wurde folgende Funktion verwendet:

static void CheckAttribute(MemberInfo memberInfo)
{
    var attributes = memberInfo.GetCustomAttributes(true);
    if (attributes.Length == 0) return;
 
    foreach (object attribute in attributes)
        Console.WriteLine("The type of the attribute in {1} is {0}.", 
            attribute, memberInfo.Name);
}

Weiterhin wurden zwei Klassen geschrieben, wo einmal eine abgeleitete Methode und eine abgeleitete Propertie mit Attributen versehen wurde:

public abstract class BaseClass
{
    [TestAttribute]
    public virtual String TestProperty { get; set; }
 
    [TestAttribute]
    public virtual void TestMethod() { return; }
}
public class DerivedClass : BaseClass
{
    public override string TestProperty { get; set; }
    public override void TestMethod() { base.TestMethod(); }
}
 
public class TestAttribute : Attribute
{
}

Zu guter Letzt wurde das ganze überprüft:

Console.WriteLine("BaseClass:");
CheckAttribute(typeof(BaseClass).GetProperty("TestProperty"));
CheckAttribute(typeof(BaseClass).GetMethod("TestMethod"));
 
Console.WriteLine("DerivedClass:");
CheckAttribute(typeof(DerivedClass).GetProperty("TestProperty"));
CheckAttribute(typeof(DerivedClass).GetMethod("TestMethod"));

Das Ergebnis fiel überraschend aus:

BaseClass:
The type of the attribute in TestProperty is TestAttrib.TestAttribute.
The type of the attribute in TestMethod is TestAttrib.TestAttribute.
DerivedClass:
The type of the attribute in TestMethod is TestAttrib.TestAttribute.

Wähernd die Methode TestMethod in beiden Klassen als mit dem Attribut ausgezeichnet, erkannt wurde, war dies bei der Eigenschaft TestProperty nicht der Fall. Um die Ursache zu finden, war ein Blick in die IL notwendig. Hier die Umsetzung der BaseClass, welche das Attribute in beiden Fällen korrekt definiert.

.method public hidebysig newslot virtual instance void TestMethod() cil managed
{
    .custom instance void TestAttrib.TestAttribute::.ctor()
}
 
.property instance string TestProperty
{
    .get instance string TestAttrib.BaseClass::get_TestProperty()
    .set instance void TestAttrib.BaseClass::set_TestProperty(string)
    .custom instance void TestAttrib.TestAttribute::.ctor()
}

Die Ableitung war aber überaschend umgesetzt. Während die Methode offensichtlich überschrieben wurde, wurde die Propertie quasi neu angelegt, aber ohne dem zugewiesenen Attribut.

.method public hidebysig virtual instance void TestMethod() cil managed
{
}
 
.property instance string TestProperty
{
    .get instance string TestAttrib.DerivedClass::get_TestProperty()
    .set instance void TestAttrib.DerivedClass::set_TestProperty(string)
}

Damit war das Ergebnis der Untersuchung: GetCustomAttributes hat definitionsgemäß auch die Ableitungen untersucht. Jedoch verlieren sich die Attribute beim Überschreiben von Properties. Eine Erklärung dafür habe ich bis jetzt nicht gefunden, ich konnte den Effekt aber in .NET 2.0 – 4.0 nachstellen.

dotnet-snippets.de wird 5 Jahre alt

24. April 2011 Keine Kommentare

Die Webseite dotnet-snippets.de wird 5 Jahre alt. Herzlichen Glückwunsch!

Wer die Seite noch nicht kennt, aber mit Microsoft .NET entwickelt, sollte einfach mal vorbeischauen. Es gibt viele kurze hilfreiche Codeschnipsel zu den meisten Problemchen, die man so im täglichen Leben hat.

KategorienAllgemein, Microsoft .NET Tags:

Alte Daten mittels Powershell löschen

13. April 2011 1 Kommentar

In einem Backup-Ordner stand die Aufgabe an, alte Dateien automatisch löschen zu lassen. Dazu wurde folgendes Skript eingesetzt:

Get-ChildItem -Path . |
  Where-Object -FilterScript {($_.LastWriteTime -lt [DateTime]::Now.AddDays(-7))} |
  Remove-Item -force

Zuerst werden alle relevanten Dateien ermittelt. Danach wird auf diese Liste ein Filter gesetzt, der nur Dateien ermittelt, die älter als 7 Tage sind. Hierzu wird eine Rückgriff auf DateTime aus dem Net-Framework genutzt. Es bietet mit Now und AddDays die gewünschte Funktionalität, von jetzt 7 Tage nach hinten zu gehen. Als letzten Schritt muss die ermittelte Liste lediglich an Remove-Item weitergereicht werden.

Erweiterung von INotifyPropertyChanged (PostSharp)

17. März 2011 4 Kommentare

Mit PostSharp bekommt man eine mächtige Möglichkeit, Code mittels Attributen aufzupeppen. So wird man auch in die Lage versetzt, wiederkehrenden Code zu vermeiden. Üblicherweise sieht nämlich beispielsweise eine Implementation von System.ComponentModel.INotifyPropertyChanged wie folgt aus:

public class Test: System.ComponentModel.INotifyPropertyChanged
{
    private string _myProp;
    public string MyProp
    {
        get { return _myProp; }
        set
        {
            if (_myProp == value) return;
            _myProp = value;
            OnPropertyChanged("MyProp");
        }
    }
 
    public event PropertyChangedEventHandler PropertyChanged;
 
    public void OnPropertyChanged(string propertyName)
    {
        if (this.PropertyChanged != null)
        {
            this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Verwendet man jedoch PostSharp als Referenz in seinem Projekt und die Klasse NotifyPropertyChangedAttribute aus

{ProgramFiles}\PostSharp 2.0\Samples\.NET Framework 3.5\CSharp\NotifyPropertyChanged

kann man seinen Code merklich verkürzen:

[NotifyPropertyChanged]
public class TestBase
{
    public string MyProp { get; set; }
}

Problem
Leider ergibt letztere Implementation das Problem, dass ich die Kontrolle über das Auslösen von Events vollständig an das Attribut NotifyPropertyChanged abgebe. Da der Code für das Event nachträglich in das Compilat injiziert wird, ergibt sich keine Möglichkeit, das Event PropertyChanged manuell auszulösen, da .NET das Auslösen von Events nur innerhalb der implementierenden Klasse erlaubt. Versuche, Zugriff auf das Event durch Vorimplementierung des Interfaces zu erhalten, helfen leider auch nicht, da der durchs Attribut erzeugte Code in einer Property der Klasse zusätzlich angelegt wird. Dies kann man leicht mit dem Reflektor prüfen:

public string MyProp
{
    [CompilerGenerated]
    get
    {
        return this.<get_MyProp>();
    }
    [CompilerGenerated]
    set
    {
        LocationInterceptionArgsImpl<string> CS$0$1 = new LocationInterceptionArgsImpl<string>(this, Arguments.Empty);
        CS$0$1.Location = <>z__Aspects.l3;
        CS$0$1.TypedBinding = <MyProp>c__Binding.singleton;
        CS$0$1.TypedValue = value;
        this.<>z__aspect0.OnPropertySet(CS$0$1);
    }
}

Das Setzen der per Attribut erweiterten Property wird beispielsweise komplett auf z__apect0 umgeleitet. Und an genau dieses z__apect0 ist ja zur Entwurfszeit noch nicht verfügbar (das ist ja der Sinn des Attributes).

Umgehen konnte ich das Problem mit einer kleiner Erweiterung des NotifyPropertyChangedAttribute:

[ImportMember("ManualPropertyChanged", IsRequired = false,
		Order = ImportMemberOrder.AfterIntroductions)]
public Event<PropertyChangedEventHandler> ManualPropertyChanged;
 
public override void RuntimeInitializeInstance()
{
	base.RuntimeInitializeInstance();
	if (ManualPropertyChanged != null)
		ManualPropertyChanged.Add((sender, args) => 
		{ if (PropertyChanged != null) PropertyChanged(Instance, args); });
}

Bietet meine Klasse, welche das NotifyPropertyChangedAttribute ausweist, folgendes Ereignis an:

public event PropertyChangedEventHandler ManualPropertyChanged;

so abonniert das Attribut dieses Ereignis und sorgt beim Auslösen von ManualPropertyChanged automatisch für eine Weiterleitung an das Ereignis PropertyChanged.
kick it on dotnet-kicks.de

KategorienMicrosoft .NET Tags: ,

Probleme beim Erstellen einer Solution

10. Oktober 2010 Keine Kommentare

Auf meinem Notebook wurde ich beim Erstellen einer Solution völlig von folgender Fehlermeldung überrascht:

The OutputPath property is not set for this project

Nach einer langen Fehlersuche bin ich dank dieses Beitrags dem Fehler auf die Spur gekommen. Rechner von HP haben (zumindest in meinem Falle) die Umgebungsvariable „Platform = HPD“. Diese muss gelöscht werden, damit Visual Studio die Solutions sauber erstellt. Ich hoffe, dass die HP-Programme danach trotzdem ohne Fehler laufen.

Probleme bei der Verwendung des VisualBasic.PowerPacks

8. September 2010 Keine Kommentare

In einer Visual Studio 2008 Anwendung habe ich die Shapes aus dem VisualBasic PowerPack verwendet, das im Lieferumfang von Visual Studio 2008 SP1 bereits enthalten ist. Nun hat sich ein Anwender aber mit folgender Fehlermeldung gemeldet, obwohl die korrekte Version des PowerPacks im Programm-Verzeichnis mit ausgeliefert wurde:

System.TypeLoadException:
 Could not load type 'Microsoft.VisualBasic.PowerPacks.ShapeContainer'
 from assembly 'Microsoft.VisualBasic.PowerPacks.Vs,
 Version=9.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'.

Nach einiger Suche war die Ursache klar. Visual Studio 2008 (ohne SP1) kennt diese DLL ebenfalls, nur leider noch ohne Shapes. Unglücklicherweise haben beide DLLs (mit und ohne SP1) die Version 9.0.0.0. Somit wird, sofern im GAC die DLL gefunden wird, automatisch immer die DLL aus dem GAC geladen. Die einzige Lösung ist darum, die DLL im GAC zu löschen oder zu aktualisieren:

Das geht, Admin-Rechte vorrausgesetzt, erstaunlich einfach per Drag & Drop.

Genauigkeit von System.Double im Zusammenspiel mit DirectX

3. September 2010 Keine Kommentare

Beim Prüfen von Quelltext sind mir wiederholt Ungenauigkeiten mit double Zahlen aufgefallen, die schon in der 8. Stelle auftraten. Nachdem ich Ausgeschlossen hatte, dass irgendwo Single-Konvertierungen stattfinden, habe ich das Problem genauer unter die Lupe genommen. Konkret ist mir aufgefallen, dass die double Rechnung 4.0 * 25.4 eigentlich 101.6 ergeben müsste, in der Zielvariable stand aber 101.59999847412109. Extern nachstellbar war das Problem anfangs nicht.

Reproduzieren ließ sich das Verhalten erst mit folgender Funktion:

private void TestDouble()
{
    var pres = new Microsoft.DirectX.Direct3D.PresentParameters
         {
             Windowed = true,
             SwapEffect = Microsoft.DirectX.Direct3D.SwapEffect.Discard,
             AutoDepthStencilFormat = Microsoft.DirectX.Direct3D.DepthFormat.D16
         };
 
    new Microsoft.DirectX.Direct3D.Device(
        0, Microsoft.DirectX.Direct3D.DeviceType.Hardware, this,
        Microsoft.DirectX.Direct3D.CreateFlags.SoftwareVertexProcessing, pres);
 
    double value = 4;
    double value2 = value * 25.4;
    Debug.Assert(101.6 == value2);
}

Das Erzeugen der 3D-Welt mittel Managed DirectX setzt standartmäßig die Genauigkeit von Double-Zahlen auf einfache Genauigkeit (entspricht Single). Nachdem diese Ursache gefunden war, fand ich hier:
http://blogs.msdn.com/b/tmiller/archive/2004/06/01/145596.aspx
schnell eine Problemlösung. Wenn man mit leichten Geschwindigkeitseinschränkungen leben kann, ist nur eine kleine Änderung beim Erzeugen vom Device notwendig:

    new Microsoft.DirectX.Direct3D.Device(
        0, Microsoft.DirectX.Direct3D.DeviceType.Hardware, this,
        Microsoft.DirectX.Direct3D.CreateFlags.SoftwareVertexProcessing | 
        Microsoft.DirectX.Direct3D.CreateFlags.FpuPreserve, pres);

Der zusätzliche Parameter Microsoft.DirectX.Direct3D.CreateFlags.FpuPreserve sorgt dafür, dass DirectX die Genauigkeit der Double-Zahlen so lässt, wie das Standardmäßig der Fall ist.

Probleme bei der Verwendung von System.Double

30. August 2010 Keine Kommentare

Bezüglich der Verarbeitung von Komma-Werten habe ich heute wieder einge Erkenntnisse gewonnen, die teilweise auch aus der verbesserten Dokumentation vom Microsoft.NET Framework 4 her rührten. Dort ist unter anderem zum Typ Double folgendes zu lesen:

Bei Verwendung einer Gleitkommazahl könnte ein Wert möglicherweise nicht wiederhergestellt werden.

Das Problem verdeutlicht folgendes gekürztes Beispiel aus der MSDN:

double fromLiteral = -4.42330604244772E-305;
double fromParse = Double.Parse("-4.42330604244772E-305");
 
Console.WriteLine("Double value from literal: {0,29:R}", fromLiteral);
Console.WriteLine("Double value from Parse method: {0,24:R}", fromParse);
 
// On 32-bit versions of the .NET Framework, the output is:
//    Double value from literal:        -4.42330604244772E-305
//    Double value from Parse method:   -4.42330604244772E-305
//
// On other versions of the .NET Framework, the output is:
//    Double value from literal:      -4.4233060424477198E-305
//    Double value from Parse method:   -4.42330604244772E-305

Das die hinteren Stellen einer Double-Zahl Ungenauigkeiten unterliegen, war mir schon länger klar. Das der selbe Code auf verschiedenen Systemen aber andere Genauigkeiten erzielen kann, war eine neue Erkenntnis, die bis dato nur als ungeäußerte und nicht beweisbare Vermutung im Raum stand.

Um das Problem zu umgehen, habe ich zwei Extensions für mich geschrieben:

public static bool NearZero(this double value)
{
    return Math.Abs(value) < 1e-12;
}
public static bool NearValue(this double value, double compareValue)
{
    if (value.NearZero() && compareValue.NearZero()) return true;
 
    return Math.Abs(value - compareValue) < 
        Math.Min(Math.Abs(value), Math.Abs(compareValue)) * 1e-12;
}

Mit der ersten prüfe ich auf 12 Stellen hinter dem Komma, ob die Zahl fast Null ist. Mit der zweiten Methode kann ich simple 2 Werte auf annähernde Gleichheit überprüfen.

Fallen der Serialisierung

29. August 2010 Keine Kommentare

Mittels des Attributes SerializableAttribute ist es sehr einfach, komplexe Objekte auf Festplatte zu speichern und später erneut zu laden. Allerdings gibt es einige Fallen, die man dabei kennen sollte:

1. Kommerzielle .NET-Anwendungen werden gern durch Codeverschlüsselung geschützt. Das bereitet natürlich Probleme, wenn solche Objekte auf Festplatte gespeichert werden. Eine spätere Verschlüsselung hat evtl. den Namen der Properties wieder geändert. Abhilfe kann hier das ObfuscationAttribute verschaffen. Damit kann man einzelne Code-Teile von der Verschlüsselung ausschließen. Damit ist der Code zwar nicht mehr vor fremden Blicken geschützt, aber das Laden/Speichern von Klassen klappt dann auch Versionsübergreifend.

2. Refaktorierung von Klassen: Klassen, die serialisiert werden, dürfen auf keinen Fall umbenannt werden. Gleiches gilt natürlich für deren Properties und Unterklassen.

VistaDB.Net’s letzte Aktualisierung

29. August 2010 Keine Kommentare

Leider hat der Hersteller von VistaDB den Vertrieb dieser Datenbank eingestellt. Als kleinen Trost konnte man zum Abschluss noch den Sourcecode sehr günstig erwerben und mit diesem auch eine unverschlüsselte Version ohne Lizenzabfragen herunterladen.

Im Folgenden möchte ich den somit letzten Umstieg von VistaDB.Net 3 auf 4.1, die bei mir noch in einigen Projekten aktiv ist, zeigen:

1. Installation von VistaDB.Net 4.1. Diese Version 4.1 ließ sich problemlos parallel zur Version 3 installieren und nutzen.

2. Upgraden der Datenbank auf die 4er Version. Das geht im VistaDB Data Builder im Menü Database unter dem Punkt Upgrade VDB3 Database sehr einfach. Leider ändert sich die Dateiendung von .vdb3 auf .vdb4. Setup-Skripte müssen hier evtl. auch angepasst werden.

3. Öffnen der Projekte in Microsoft Visual Studio. In den Projekten ist ein Verweis auf VistaDB.NET20 vorhanden. Dieser wird entfernt und durch den VistaDB 4 Provider ersetzt.

4. Die Datasets (.xsd) müssen in einem externen Editor geöffnet werden, um den XML-Code direkt zu bearbeiten. Man findet darin mindestens einmal Provider=“VistaDB.NET20″, was durch Provider=“System.Data.VistaDB“ ersetzt werden muss.

5. Jetzt kommt der schwierigste Teil. Die Connectionstrings müssen aktualisiert werden und die Datasets nochmal neu geschrieben werden. Dazu reicht es eigentlich, den Connectionsstring in den Einstellungen auf die neue Datei (die Dateiendung hat sich ja geändert) zu setzen und einen TableAdapter im Dataset neu zu konfigurieren (ohne Änderung). Jedoch war bei mir das Problem, dass die ConnectionsStrings nicht mehr gültig waren. Das ist zwar ein Problem von Visual Studio und nicht von VistaDB, es steht hier aber trotzdem im Weg. Darum gehe ich einen anderen Weg. Darum habe ich einen anderen Weg gewählt.

Ich habe mir den Namen des Connectionstrings notiert und ihn aus den Einstellungen gelöscht. Dann habe ich ein neues Dataset erzeugt und dort den Connectionstring neu eingegeben und unter dem alten Namen gespeichert. Damit wurde dann als Dummy ein Tableadapter konfiguriert. Das Dataset kann zum Schluss wieder gelöscht werden. Nun können die alten Datasets geöffnet und ein Tableadater neu erzeugt werden.

KategorienDatenbank, Microsoft .NET Tags: