Laden...

Darstellung von Objekten im Canvas mit MVVM

Erstellt von BJA-CH vor 5 Jahren Letzter Beitrag vor 5 Jahren 2.694 Views
B
BJA-CH Themenstarter:in
59 Beiträge seit 2017
vor 5 Jahren
Darstellung von Objekten im Canvas mit MVVM

Also ich habe da mal einen Versuch gemacht, wie ich meine Planeten in einem Canvas mit beliebiger Grösse darstellen kann.
Mein versuch sieht so aus:

            <!--Testbeispiel Plaenten-->
            <ItemsControl Name="canvasPlaneten" ItemsSource="{Binding Path=ItemsPlanetes}"
                          Width="400" Height="200" Background="LightBlue">
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <Canvas/>
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
                <ItemsControl.ItemContainerStyle>
                    <Style TargetType="ContentPresenter">
                        <Setter Property="Canvas.Left">
                            <Setter.Value>
                                <MultiBinding Converter="{StaticResource posRaConverter}">
                                    <Binding Path="Ra"/>
                                    <Binding ElementName="canvasPlaneten" Path="ActualWidth"/>
                                    <Binding ElementName="canvasPlaneten" Path="ActualHeight"/>
                                    <Binding Path="Radius"/>
                                </MultiBinding>
                            </Setter.Value>
                        </Setter>
                        <Setter Property="Canvas.Top">
                            <Setter.Value>
                                <MultiBinding Converter="{StaticResource posDecConverter}">
                                    <Binding Path="Dec"/>
                                    <Binding ElementName="canvasPlaneten" Path="ActualWidth"/>
                                    <Binding ElementName="canvasPlaneten" Path="ActualHeight"/>
                                    <Binding Path="Radius"/>
                                </MultiBinding>
                            </Setter.Value>
                        </Setter>
                    </Style>
                </ItemsControl.ItemContainerStyle>
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Ellipse Height="{Binding Radius}" Width="{Binding Radius}" Fill="{Binding Color}" Stroke="{Binding Color}"/>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>

Also MultiValueConverter für die Berechnung der Position im Canvas habe ich folgende Klasse gebildet (Beispiel Rektaszension):

        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            double _height100 = 845;
            double _width100 = 1620;

            double ra = System.Convert.ToDouble(values[0]);
            double width = System.Convert.ToDouble(values[1]);
            double height = System.Convert.ToDouble(values[2]);
            double radius = System.Convert.ToDouble(values[3]);

            double faktorHeight = height / _height100;
            double faktorWidth = width / _width100;
            double faktor = faktorHeight < faktorWidth ? faktorHeight : faktorWidth;

            return (width / 24.0 * (24.0 - ra) - (radius * faktor));
        }

Läuft eigentlich erstaunlich gut, so wie MrSparkle vorausgesagt hat.
Was ich nun aber nicht hinkriege ist die aktuelle Grösse der Canvas-Fläche zu meinem Converter zu bringen.
Deshlab nun meine Fragem:
a) Woher nehme ich die aktuelle Grösse der Zeichenfläche;
b) Ist dieser erste Ansatz für eine dynamische MMVM-Canvas-Darstellung richtig?

Danke für eure Hilfe!

W
955 Beiträge seit 2010
vor 5 Jahren

a) Woher nehme ich die aktuelle Grösse der Zeichenfläche;

* Du könntest das Canvas als ConverterParameter übergeben.
* Du könntest Width/Height des Canvas und die Berechnung komplett in das VM verlagern

2.078 Beiträge seit 2012
vor 5 Jahren

Verstehe ich das richtig, dass Du als vierten Wert dem Converter die Canvas-Fläche mit geben willst?

Getestet habe ich es nicht, aber ich würde im ersten Schritt ein Binding mit ElementName=CanvasPlaneten schreiben.
Dieses Binding bekommt dann einen ValueConverter, der anhand des ItemsControls das ItemsPanel sucht. Wie das geht wird hier erklärt.

So bekommt dein MultiValueConverter das Canvas als Wert mit gegeben.
Wenn Du das nicht möchtest, kannst Du den Umweg über einen BindingProxy gehen, wo Du das Binding wie oben beschrieben definierst. Später kannst Du dann diesen BindingProxy als Source bei anderen Bindings (also für deine MultiValueConverter) verwenden und den konkreten Path angeben.

Grob im Browser herunter getippt:

<!-- In den Ressourcen: -->
<local:BindingProxy x:Key="canvasBindingProxy" Data="{Binding ElementName=CanvasPlaneten, Converter={local:GetItemsPanelConverter}}" />

<!-- Im Style: -->
<MultiBinding Converter="{StaticResource posDecConverter}">
    <Binding Path="Dec"/>
    <Binding ElementName="canvasPlaneten" Path="ActualWidth"/>
    <Binding ElementName="canvasPlaneten" Path="ActualHeight"/>
    <Binding Path="Radius"/>
    <Binding Path="Data.ActualWidth" Source="{StaticResource canvasBindingProxy}" />
    <Binding Path="Data.ActualHeight" Source="{StaticResource canvasBindingProxy}" />
</MultiBinding>

Ist zugegeben etwas umständlich, aber so könnte es funktionieren.

Ich persönlich würde das aber nicht mit einem MultiValueConverter machen, sondern stattdessen ein eigenes DependencyObject schreiben, was für die einzelnen Werte DependencyProperties hat - und eine readonly DependencyProperty, die das Ergebnis ausspuckt.
Beim MultiValueConverter sehe ich den Nachteil, dass Du nicht klar erkennen kannst, welches Binding welche Bedeutung hat und auch die Reihenfolge von wichtiger Bedeutung ist.

PS:
Beim BindingProxy hab ich den Converter direkt als MarkupExtension zugewiesen.
Das kannst Du auch, wenn dein Converter von der Klasse "MarkupExtension" ableitet. Die "ProvideValue"-Methode gibt dann einfach this zurück.

PPS:
Man sagt, dass die Performance unter einem Converter als MarkupExtension leidet. Theoretisch mag das auch stören, ich hatte bisher aber keine Probleme deswegen (und ich mach das häufig), daher würde ich hier der mMn. besseren Übersichtlichkeit den Vorzug geben, als der minimal besseren Performance.
Gibt's da Mal Probleme, kann man das ja immer noch anpassen, indem Man den Converter als Ressource einträgt und die Ressource verwendet, die Ableitung von MarkupExtension muss dafür nicht entfernt werden.

5.657 Beiträge seit 2006
vor 5 Jahren

Die Canvas positioniert Elemente absolut, nicht relativ zur Größe. Du kannst eine Zoom-Funktion verwenden, damit der Benutzer rein- oder rauszoomen kann. Dafür gibt es Beispiele im Internet.

Ansonsten gehört die Logik zur Berechnung der Größen und Positionen in das ViewModel, und nicht in einen Converter.

Dann kannst du auch Canvas.ActualWidth bzw. ActualHeight an eine Eigenschaft deines ViewModels binden (OneWay, weil schreibgeschützt), um den Zoomfaktor automatisch im ViewModel zu berechnen.

Weeks of programming can save you hours of planning

B
BJA-CH Themenstarter:in
59 Beiträge seit 2017
vor 5 Jahren

Danke alle für eure Antwort.
Aber nun habe ich einen Knopf. Was ist nun View (Ansicht) und was ist ViewModel (Datensicht),
Meine Überlegung war, die physikalische Bedeutung der Daten gehört ins ViewModel, da es die Datensicht bedeutet. Die Umrechnung der Daten auf die momentane Fenstergrösse gehört zur Ansicht. Habe ich da dann doch was Falsches verstanden?
Wenn dem nicht so ist, dann sehe ich den Nutzen einer solchen "Verkomplizierung" nicht.... Ich meine ich habe den Sternenhimmel mit allen Zusatzkomponenten ganz normal in einer Benutzersteuerelement gebaut... und mir scheint, es ist auch nicht die schlechteste Lösung...
Stelle mal ein Bild in den Anhang...

B
BJA-CH Themenstarter:in
59 Beiträge seit 2017
vor 5 Jahren

So den Anhang habe ich nun auch gepackt.... 256KB sind halt wirklich nicht mehr viel... Hoffe man kann noch was erkennen...

5.657 Beiträge seit 2006
vor 5 Jahren

Sieht schick aus.

Aber zu deiner Frage: Die View ist das, was du siehst. Also die Elemente in der XAML-Datei. Die Berechnung gehört zur Logik, d.h. ins ViewModel. Converter sind wiederum dafür gedacht, Daten zwischen unterschiedlichen Typen zu konvertieren.

Ich würde es so implementieren, wie ich geschrieben hatte:

Canvas.ActualWidth bzw. ActualHeight an eine Eigenschaft deines ViewModels binden

Weeks of programming can save you hours of planning

B
BJA-CH Themenstarter:in
59 Beiträge seit 2017
vor 5 Jahren

OK, also ich nehme mir nun die Zeit und baue meinen "Himmel" um. Ich sehe den Vorteil, dass, wenn es klappt, ein Benutzersteuerelement nicht nötig sein wird, da einiges einfacher werden wird.
Einiges läuft schon... aber bis nur ein schreibgeschütztes ActualWidth und ActualHeight angebunden war... aber geschafft!

Nun habe ich noch ein Frage. Wie in meinem Bild ersichtlich verwende ich ja nicht bloss ein grafisches Element im Bild. Ich benötige Kreise (Ellipsen), Linie, TextBlock und Polyline.
Gibt es eine Möglichkeit im DataTemplate des ItemsControl eine Weiche einzubauen, mit der jeweilige Control-Typ ausgewählt werden kann? Oder baut man pro Control-Typ (Ellipse, Linie... ) ein eigenes ItemsControl ein?
Alle Beispiele, welche ich gefunden habe die verwenden für alle Elemente den gleichen Control-Typ und meinst nur ein TextBlock...

W
955 Beiträge seit 2010
vor 5 Jahren

Du kannst mal schauen ob es ein TemplateSelektor für das ItemTemplate gibt. Alternativ könntest Du versuchen mit Paths zu arbeiten, jedes geometrisches Objekt gibt sich als Path aus, das dann gebunden wird (vllt mit VisualBrush).

B
BJA-CH Themenstarter:in
59 Beiträge seit 2017
vor 5 Jahren

MrSparkle, ich habe eine Frage an deinen Beitrag zu meinem Problem.
Es ist richtig, ich kann ActualHeight und ActualWidth in das ViewModel holen (binden) und dort die Umrechnung auf die Pixel machen. Das funktioniert sogar. Hat aber den Nachteil. dass bei einer "SizeChanged" alles per Code nachgerechnet werden muss.
Sehe ich dies falsch?
Ich denke nur, wenn das Konzept nun "binden zum Teufel komm raus" heissen soll, so müsste doch die Grösse der Zeichnungsfläche auch gebunden sein. So aber muss ich nun ein Code schreiben, der bei einer Veränderung der Fenstergrösse die Position und Grösse jedes Objektes nachrechnen...

5.657 Beiträge seit 2006
vor 5 Jahren

Gibt es eine Möglichkeit im DataTemplate des ItemsControl eine Weiche einzubauen, mit der jeweilige Control-Typ ausgewählt werden kann?

Ja: DataTemplates. Siehe den entsprechenden Abschnitt in [Artikel] MVVM und DataBinding

Hat aber den Nachteil. dass bei einer "SizeChanged" alles per Code nachgerechnet werden muss.

Klar mußt du deine Positions-Berechnung aktualisieren, wenn sich die Größe geändert hat. Aber wenn du das DataBinding richtig umgesetzt hast, funktioniert das alles von allein. Alle Konzepte, die du dazu brauchst, sind aber in dem verlinkten Artikel beschrieben und in dem dazugehörigen Beispielprojekt umgesetzt. Wenn dir das mit den grafischen Inhalten zu kompliziert ist für den Anfang, dann kannst du es auch erstmal in einem kleinen Testprojekt ausprobieren und testen.

Weeks of programming can save you hours of planning