Entwickler-Ecke

Basistechnologien - Eigenschaft mit mehreren Werten anzeigen


Delete - Sa 06.08.16 02:13
Titel: Eigenschaft mit mehreren Werten anzeigen
- Nachträglich durch die Entwickler-Ecke gelöscht -


Christian S. - Sa 06.08.16 09:48

Hallo,

mit einem struct wird es vermutlich gar nicht gehen, weil der ja kein Referenzdatentyp ist und an Kopien gearbeitet wird. Bei einer Klasse musst Du Deinen eigenen TypeConverter schreiben. Das ist hier ganz gut gezeigt: https://social.msdn.microsoft.com/Forums/windows/en-US/f81b6caa-5fae-45c4-ad46-2240e84a5d7a/how-to-expand-nested-complex-types-in-propertygrid?forum=winforms

Im Prinzip so (anhand Deiner frListView-Komponente, die ich noch in meiner Test-Umgebung hatte :D)

C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
    public class frListView : ListView
    {
        [TypeConverter(typeof(MySettingsTypeConverter))]
        public class MySettings
        {
            [Browsable(true)]
            public int Setting1 { get; set; }
            [Browsable(true)]
            public string Setting2 { get; set; }
        }


        public class MySettingsTypeConverter : TypeConverter
        {
            public override object ConvertTo(ITypeDescriptorContext context,
            System.Globalization.CultureInfo culture, object value, Type destinationType)
            {//This method is used to shown information in the PropertyGrid.
                if (destinationType == typeof(string))
                {
                    return ((MySettings)value).Setting1.ToString() + "," + ((MySettings)value).Setting2;
                }
                return base.ConvertTo(context, culture, value, destinationType);
            }

            public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes)
            {
                return TypeDescriptor.GetProperties(typeof(MySettings), attributes).Sort(new string[] { "Setting1""Setting2" });
            }

            public override bool GetPropertiesSupported(ITypeDescriptorContext context)
            {
                return true;
            }
        }

        [Browsable(true)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
        public MySettings MySetting { get; private set; }

        public frListView()
        {
            MySetting = new MySettings();
        }

/* ... */


Die ListView hat jetzt eine im Properties-Fenster editierbare Eigenschaft "MySetting" vom Typ "MySettings". Damit das Properties-Window weiß, was es zu tun hat, gibt es den TypeConverter, der vor allem für die String-Darstellung sorgt und eine Liste mit editierbaren Properties zurückgibt.

Viele Grüße
Christian


Delete - Sa 06.08.16 10:13

- Nachträglich durch die Entwickler-Ecke gelöscht -


Palladin007 - Sa 06.08.16 11:40

Nochmal wegen dem Struct:
Ein Struct ist fast immer eine ganz schlechte Idee
Hier [https://msdn.microsoft.com/en-us/library/ms229017(v=vs.110).aspx] findest Du weiter unten eine Checkliste, wann man Structs nehmen kann/sollte
Diese Punkte habe ich bisher aber noch nie sinnvoll erfüllt gesehen, außer bei .NET eigenen Typen wie Point, Size oder DateTime.


Delete - Sa 06.08.16 12:14

- Nachträglich durch die Entwickler-Ecke gelöscht -


Ralf Jansen - Sa 06.08.16 12:17

Zitat:
Diese Punkte habe ich bisher aber noch nie sinnvoll erfüllt gesehen, außer bei .NET eigenen Typen wie Point, Size oder DateTime.


Eigentlich scheitert Microsoft fast immer an der immutability (auch Point&Size) ;) Außer vielleicht bei elementaren Typen die man für Basistypen halten könnte wie DateTime. Und wenn sie es richtig machen sind die Leute doch nicht zufrieden weil es andere ~Dinge~ verhindert. Z.B die ~richtige~ struct Implementierung von KeyValuePair die Serialisieren von Dictionarys problematisch macht.


Palladin007 - Sa 06.08.16 14:48

Point ist veränderbar?
In meinem Verständnis sollte alles ein Struct sein, das einen einzelnen Wert darstellt, wie die Zahl 5 eben immer und nur die Zahl 5 sein kann.
Ein Punkt an den Koordinaten 12 und 13 wird immer diese Koordinaten haben, sonst ist es ein anderer Punkt.
Bei Size - gut, da wäre eine Klasse vermutlich doch die bessere Wahl, aber bei WPF haben sie die Size-Property ja auch raus geworfen und belassen aus bei Width und Height.
Warum KeyValuePair ein Struct ist, verstehe ich gar nicht :D


@Frühlingsrolle:

Ich kenne Objective Pascal nicht, den Code aus dem Link kapier ich auch nicht so wirklich :D
Der TypeConverter hat aber an sich nichts damit zu tun, dass unter C# etwas nicht oder nur anders lösbar ist, sondern dass der TypeConverter von Controls wie dem PropertyGrid verwendet wird um die Eingabe von dem Benutzer richtig zu konvertieren.
Das ist das Problem bei Controls, die so viel automatisch machen, Du gibst massiv Kontrolle ab, weil Du dadurch ebenfalls massiv Arbeit sparst.
Der TypeConverter soll bei der Arbeit die Möglichkeit bieten, einen Teil dieser Kontrolle zurück zu bekommen.

Ein Beispiel, das ich letztens gebraucht habe:
Ich hatte ein int-Array, das in dem PropertyGrid angezeigt werden soll. Dort wird das automatisch über ein zweites Fenster zugänglich gemacht, über das man dann einzeln Zahlen hinzufügen oder entfernen kann. Wie ich finde eine denkbar unschöne Variante, ich brauch doch kein eigenes kleines Fenster um ein paar Zahlen zu definieren.
Meine TypeConverter-Lösung hat das ganze erweitert, indem der Nutzer zusätzlich die Zahlen kommagetrennt schreiben kann und die Zahlen dann als int-Array konvertiert und verwendet werden.
Das ließe sich allerdings auch so lösen:


C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
[Browsable(false)]
public int[] Array { get; set; }

[Browsable(true)]
[DisplayName("Array")]
public string ArrayString
{
    get { return string.Join(",", Array); }
    set { Array = value.Split(',').Select(int.Parse).ToArray(); }
}


Angezeigt wird dann die Property ArrayString unter dem Namen Array, die eigentliche Array-Property ist gar nicht sichtbar, wird aber trotzdem korrekt gefüllt.
Soweit ich das beurteilen kann, kommt das denke ich dem Beispiel aus dem Link am nächsten?


Ralf Jansen - Sa 06.08.16 15:17

Zitat:
Ein Punkt an den Koordinaten 12 und 13 wird immer diese Koordinaten haben, sonst ist es ein anderer Punkt.


So sollte es sein und die Implementierung von Point als struct täuscht das auch vor. Trotzdem haben die Koordinaten von Point einen setter und implizieren was anderes und Programmierer werden deswegen glauben es wäre sinnvoll änderbar. structs + setzbare Properties sind immer ein Design Problem. Manchmal vielleicht nötig und das Point/Size so gebaut sind wie sie sind hat möglicherweise einen technischen Grund aus dem Framework heraus. Der erschließt sich mir aber rein logisch erstmal nicht so das ich ihn gerade nicht benennen kann.
Das war übrigens auch der einzige Punkt :D den ich setzen wollte. Du stelltest Point/Size als positive Beispiele heraus wie man struct implementieren sollte. Das sind sie sicher nicht. Wenn man structs implementiert dann ohne Setter für Properties (als echten immutable Typ). Zumindest sollte man es nicht grundlos tun.


Palladin007 - Sa 06.08.16 15:27

Ok, ich entschuldige mich für diese Fehlinformation :/

Ich wusste bis jetzt nicht einmal, dass die Setter haben O.o


Ralf Jansen - Sa 06.08.16 15:35

Zitat:
Ok, ich entschuldige mich für diese Fehlinformation


Kein Problem und auch kein Vorwurf von mir. Wie, wo, warum structs ist ein spannendes und problematische Thema.


Delete - Mo 08.08.16 04:22

- Nachträglich durch die Entwickler-Ecke gelöscht -


Palladin007 - Mo 08.08.16 08:11

Wie meinst Du das?

Solange der TypeConverter den Typ der Property in den Ziel-Typ (beim PropertyGrid wäre es dann wohl String) konvertieren kann, kannst Du den so oft nutzen wie Du willst.
Das Attribute TypeConverter definiert ja nur, wo der COnverter zu finden ist, das PropertyGrid erzeugt sich den dann selber.


Delete - Mo 08.08.16 09:47

- Nachträglich durch die Entwickler-Ecke gelöscht -


Palladin007 - Mo 08.08.16 10:16

Schreib dir doch eine Basis-Klasse?
Die hat dann z.B. zwei abstrakte Methode "GetSetting1" und "GetSetting2"
Der Rest kann ja gleich bleiben


Delete - Mo 08.08.16 13:24

- Nachträglich durch die Entwickler-Ecke gelöscht -


Christian S. - Mo 08.08.16 14:46

Vermutlich kann man die verschiedenen Einstellungstypen ein gemeinsames Interface implementieren lassen und dann einen generischen TypeConverter schreiben, der für alle Typen funktioniert, die dieses Interface implementieren. Wenn bis dahin keine bessere Lösung kommt, schreibe ich da heute abend mal was für zusammen.


Delete - Mo 08.08.16 17:49

- Nachträglich durch die Entwickler-Ecke gelöscht -


Christian S. - Mo 08.08.16 18:48

Hallo,

so habe ich mir das gedacht:


C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
[TypeConverter(typeof(GenericTypeConverter<MySettings>))]
    public class MySettings : ITypeConvertible
    {
        [Browsable(true)]
        public int Setting1 { get; set; }
        [Browsable(true)]
        public string Setting2 { get; set; }

        public string[] GetProperties()
        {
            return new string[] { nameof(Setting1), nameof(Setting2) };

        }

        public string GetPropertiesAsString()
        {
            return $"{Setting1}, {Setting2}";
        }
    }

    public interface ITypeConvertible
    {
        string[] GetProperties();
        string GetPropertiesAsString();
    }

    public class GenericTypeConverter<T> : TypeConverter where T : ITypeConvertible
    {
        public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
        {
            if (value.GetType() != typeof(T))
                throw new ArgumentException($"{nameof(value)} has the wrong type.", nameof(value));

            if (destinationType == typeof(string))
            {
                return ((T)value).GetPropertiesAsString();
            }

            return base.ConvertTo(context, culture, value, destinationType);
        }

        public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes)
        {
            if (value.GetType() != typeof(T))
                throw new ArgumentException($"{nameof(value)} has the wrong type.", nameof(value));

            return TypeDescriptor.GetProperties(typeof(T), attributes).Sort(((T)value).GetProperties());
        }

        public override bool GetPropertiesSupported(ITypeDescriptorContext context)
        {
            return true;
        }
    }


Grüße
Christian


Delete - Mo 08.08.16 21:12

- Nachträglich durch die Entwickler-Ecke gelöscht -


Delete - Do 11.08.16 00:41

- Nachträglich durch die Entwickler-Ecke gelöscht -


jaenicke - Do 11.08.16 07:32

user profile iconFrühlingsrolle hat folgendes geschrieben Zum zitierten Posting springen:
Das Problem besteht nicht darin, nicht zu wissen, wann ich ein struct und wann eine class verwenden soll, vielmehr fällt es mir schwer, Dinge in .NET umzusetzen, die in Objective Pascal leichter/anders von der Hand gehen. Dort wäre ich auf den TypeConverter nicht angewiesen und könnte sehr wohl auf die Art eine Eigenschaft bereitstellen
Eigentlich ist der Unterschied zwischen Delphi und .NET an der Stelle relativ klein. In beiden Fällen wird bei Verwendung in einer property eine Kopie des structs beziehungsweise Records zurückgegeben, deren Eigenschaften man nicht verändern kann.

Und auch für den Objektinspektor brauchst du dann eine entsprechende Hilfsklasse um Dsten des Records anzuzeigen oder zu modifizieren.


Delete - Do 11.08.16 12:51

- Nachträglich durch die Entwickler-Ecke gelöscht -


Ralf Jansen - Fr 12.08.16 21:29

Im generischen TypeConverter fehlt irgendwie der Weg zurück? Man muss ja nicht nur seinen Typ in einen string umwandeln können sondern auch zurück also string nach Typ.
Ich hab mal dein Beispiel vervollständigt mit den Dingen die ich für fehlend halte.

Ich muss aber sagen das ich die Idee merkwürdig finde den Designtime Code im eigentlichen Typen unterzubringen.
Der muss im späteren Code nicht mit ausgeliefert werden sollte daher woanders hin (andere Assembly), der TypeConverter selber wäre da der übliche verdächtige. Einfach einen BasisConverter erstellen und dann davon Ableitungen erzeugen für die konkreten Klassen. Ist nicht mehr Code als so und der Design Code ist sauber vom eigentlichen Code getrennt.

Um eine Instanz des Typen zu erzeugen ist so auch ein doofer Hack nötig. Man kann natürlich ein Methode zum erzeugen dem Interface hinzufügen aber das gehört ja zwingend zu einer Instanz der Klasse um also über ein Interface einen passenden Typen erzeugen zu können muss man erstmal eine Instanz erzeugen (über den Standardkonstruktor) um eine passende Instanz erzeugen zu können :cry:


Delete - Sa 13.08.16 17:10

- Nachträglich durch die Entwickler-Ecke gelöscht -