Laden...

Elemente dynamisch hinzufügen + virtualisierung

Erstellt von Quaneu vor 9 Jahren Letzter Beitrag vor 9 Jahren 6.568 Views
Quaneu Themenstarter:in
692 Beiträge seit 2008
vor 9 Jahren
Elemente dynamisch hinzufügen + virtualisierung

Hallo zusammen,

ich will ein eigenes Control bauen, indem die Elemente zur Laufzeit hinzugefügt werden und bei zu vielen Elementen virtualisiert. D.h. Ich binde mich an eine Liste mit n Elementen und es werden z.B. n TextBoxen geniert und dem Control hinzugefügt. Wenn nun aber nur m angezeigt werden können (m<n) so sollen auch nur m gezeichnet werden.

Ich habe mir schon einige Gedanken gemacht, und habe nun einen Favoriten, doch weiß ich nicht ob diese "richtig" ist.

Ich würde es gerne über MeasureOverride() und ArrangeOverride() lösen. Doch leider scheint dies nicht ohne weiteres zu gehen, bzw. stellt sich mir hier eine Performance frage.
Bei MeasureOverride() ruft man unter anderem Measure() auf allen Kindern auf. Doch diese müssen erst hinzugefügt werden und auf allen ApplyTemplate() aufgerufen werden. Danach liefert mir DesiredSize die richtige Größe zurück, mit der ich nun die Elemente bestimmen kann, die wirklich gezeichnet werden sollen... D.h. ich muss die Elemente die nicht gezeichnet werden sollen wieder entfernen... All das erscheint mir nicht sauber... Daher auch mein Frage...

Über Tipps wäre ich sehr dankbar.

Schöne Grüße
Quaneu

211 Beiträge seit 2008
vor 9 Jahren

Grundprinzip ist soweit schon richtig, du wirst eine Collection benötigen die du an ein ItemsControl übergibst und diese soll sich um die Darstellung kümmern.

Der Container den du dabei verwenden solltest ist zum Beispiel das eingebaute "VirtualizingStackPanel" im .NET Framework da eben dieses nur die Elemente rendert die im unmittelbaren Bereich sichtbar sind.

Dementsprechend Nutze ein ItemsControl und verwende da ein VirtualizaingStackPanel (siehe hier: Link) dann hast du dein Performanceproblem gelöst.

Kontakt & Blog: www.giesswein-apps.at

Quaneu Themenstarter:in
692 Beiträge seit 2008
vor 9 Jahren

Danke für Deine schnelle Antwort.

Leider stehe ich immer noch auf dem Schlauch...


private const FrameworkPropertyMetadataOptions metadata =
    FrameworkPropertyMetadataOptions.AffectsMeasure |
    FrameworkPropertyMetadataOptions.AffectsArrange;

// Dependency Property
public static readonly DependencyProperty MatrixProperty =
        DependencyProperty.Register("Matrix",
        typeof(MatrixInfo),
        typeof(MatrixControl),
        new FrameworkPropertyMetadata(null, metadata, MatrixChanged));

// .NET Property wrapper
public MatrixInfo Matrix
{
    get { return (MatrixInfo)GetValue(MatrixProperty); }
    set { SetValue(MatrixProperty, value); }
}

private static void MatrixChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
    MatrixControl matrixControl = sender as MatrixControl;
    Canvas canvas = matrixControl._canvas;
    MatrixInfo matrixInfo = e.NewValue as MatrixInfo;

    foreach (RowItem rowItem in matrixInfo.RowItems)
    {
        RowControl rowControl = new RowControl(rowItem);
        canvas.Children.Add(rowControl);
    }

    foreach (ColumnItem columnItem in matrixInfo.ColumnItems)
    {
        ColumnControl columnControl = new ColumnControl(columnItem);
        canvas.Children.Add(columnControl);
    }

    foreach (ContentItem contentItem in matrixInfo.ContentItems)
    {
        ContentControl contentControl = new ContentControl(contentItem);
        canvas.Children.Add(contentControl);
    }
}

Hier lege meine Controls an und füge sie dem Canvas hinzu. Nun will ich die beiden Methoden verwenden.


protected override Size ArrangeOverride(Size arrangeBounds)
{
    return base.ArrangeOverride(arrangeBounds);
}

protected override Size MeasureOverride(Size constraint)
{
    return base.MeasureOverride(constraint);
}

Doch leider wird erst nach ArrangeOverride() die OnApplyTemplate() der Controls aufgerufen. Diese muss jedoch schon vor dem Aufruf von MeasureOverride() aufgerufen werde, da diese sonst nicht richtig funktioniert...

Wo liegt mein Fehler?

Schöne Grüße
Quaneu

P
157 Beiträge seit 2014
vor 9 Jahren

Hallöchen,

FrameworkPropertyMetadataOptions metadata =    FrameworkPropertyMetadataOptions.AffectsMeasure |    FrameworkPropertyMetadataOptions.AffectsArrange;

Soweit ich mich erinnere sorgen die Flags dafür dass nach dem Aufruf deiner Changed-Methode die entsprechenden Neuberechnungen automatisch aufgerufen werden.

Wenn du die Virtualisierung nur über eine einfache Anzahl von Textboxen aktivieren möchtest, verwende einfach einen Trigger, das ist weitaus einfacher.

Aber ich glaub du kannst dir den Aufwand sparen, dort die Childs von den Canvas zu befüllen.

Ich vermute mal du hast ein ControlTemplate für dein MatrixControl geschrieben ?!...dort müsstest du den Canvas per TemplateBinding befüllen können, alternativ mit einem RelativeSourceBinding (gibts auch für TemplatedParent - TemplateBinding ist nur die "Kurzversion" davon).

Soweit ich das erkenne, befüllst du aus der MatrixInfo-Klasse die Child-Collection, in dem du dort für eine Daten-Implementierung einen fest vergibst. Dadurch verhinderst du aber dass du eigene DataTemplates verwenden kannst. So bist du darauf angewiesen immer wieder dein Custom-Control anzupassen wenn du eine Erweiterung machen möchtest.

Ein kleiner Hinweis noch: wenn sich deine Infoklasse ändert, fügst du alle Elemente der Infoklasse zum Canvas hinzu, löschst die Alten aber nicht. Würde bedeuten, wenn du die Eigenschaft erneut setzt, bleiben alle Objekte vom alten bestehen.

Die Entwicklung von eigenen Steuerelementen ist verzwickter, als ein einfaches UserControl, da es dort weitaus mehr Regeln zu beachten gibt. Es gibt einige Annotations für die Entwicklung von CustomControls(Defaults von Binding für Collections) und Namenskonventionen (PART_), sowie die Verwendung von externen Data/Style-Templates und CommandBindings.

Alternativ kannst du dein Matrixzeugs auch über ein AttachedProperty an einen Canvas hängen, da musst du dir nich das Control schreiben und über einen Trigger auf die Anzahl deine Textboxen kannst du die Virtualisierung an/aus schalten.

Wiki mal Decorator pattern, falls du das noch nicht kennst...es hilft beim verständnis der WPF UI Architektur.

vg

Wenn's zum weinen nicht reicht, lach drüber!

Quaneu Themenstarter:in
692 Beiträge seit 2008
vor 9 Jahren

Hallo Parso,

vielen Dank für Deine Tipps und Hinweise.
Das mit den DataTemplates gefällt mir sehr gut und ich werde versuchen, dies so umzusetzen.

Für das MatrixControl habe ich ein ControlTemplate, da dies ja ein eigenes Steuerelement werden soll.

Schöne Grüße
Quaneu

Quaneu Themenstarter:in
692 Beiträge seit 2008
vor 9 Jahren

So jetzt muss ich leider nochmal fragen 😃

Soweit ich das erkenne, befüllst du aus der MatrixInfo-Klasse die Child-Collection, in dem du dort für eine Daten-Implementierung einen fest vergibst. Dadurch verhinderst du aber dass du eigene DataTemplates verwenden kannst. So bist du darauf angewiesen immer wieder dein Custom-Control anzupassen wenn du eine Erweiterung machen möchtest.

Ich muss dies leider machen, da ich sonst die Größe der Controls nicht ermitteln. Diese brach ich jedoch um zu ermitteln, ob sie noch gezeichnet werden sollen:


private static void MatrixChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
       ...

	foreach (RowItem rowItem in matrixInfo.RowItems.OrderBy(x => x.Index))
	{
		RowControl rowControl = new RowControl(rowItem);
		grid.Children.Add(rowControl);
		rowControl.ApplyTemplate();
		rowControl.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
	}

       ...

Oder verstehe ich Dich nicht ganz? Sorry...

Grüße
Quaneu

P
157 Beiträge seit 2014
vor 9 Jahren

Ok nochmal, vielleicht ist es so verständlicher :

    foreach (RowItem rowItem in matrixInfo.RowItems.OrderBy(x => x.Index))
    {
        RowControl rowControl = new RowControl(rowItem);
        grid.Children.Add(rowControl);
        rowControl.ApplyTemplate();
        rowControl.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
    }

Du möchtest die größe deiner Elemente berechnen, das brauchst du nicht!

rowControl.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity)); << normalerweise kommen dort die abmessungen des Containers hinein...also deines Canvas. Da der sich aber immer verändern kann, kommen am Ende auch andere Werte raus, je nach dem wie du deine Allignments angegeben hast(stretch/left/right) die wieder von den Abmessungen des Parents abhängig sind.

Das ist etwas das dir das WPF abnimmt. Wichtig ist dabei, dass du verstehst wie dieses Teil arbeitet. Sonst machst du "Workarounds" --- Euphemismus für Hacks..

Wir gehen mal von folgendem theoretischem Fall aus :

Du hast eine Liste von Elementen. Diese Elemente sollen in einem Container dargestellt werden.

Für die Darstellung verwenden wir ein ListView und deine Elemente sind einfache Strings.
Wenn du dir nun ein Control schreibst, wirst du die Liste als ItemsSource angeben und für die Darstellung wirst du ein DataTemplate verwenden.

Nun wird das DataTemplate auf eine Element der Liste angewendet, dadurch ergeben sich bestimmte Boundaries - diese sind die Größe deiner Elemente in der Visualisierung. Das ListView weis somit auch erst nach der Anwendung des DataTemplates die Größe seines Childs-Darsteller...und das auch erst wenn es sich selbst zeichnet. Vorher wird es nämlich nicht gebraucht.

Die Anordnung deiner Elemente wird vom Listview vorgenommen, da dort der Layout - Mechanismus bekannt ist, so sind die Koordinaten und Abmessungen nächsten Zeichnen wieder andere.

Die Architektur von WPF-UI unterscheidet sich enorm zu der Winforms - Architektur. In WPF ist alles etwas abstrakter gehalten, du musst dort keine Pixelschieberei betreiben. Du möchtest ja im Endeffekt nur, dass deine Elemente an einer bestimmten Stelle stehen, also versuch einfach zu überlegen : was muss ich dafür tun und was nicht.

Wenn man das so salopp ausdrückt:
Viele Entwickler denken noch in Pixeln und Events und UI-CodeBehind...das ist der Grund wieso die Lernkurve von WPF so riesig ist(was der Bauer nicht kennt, frisst er nicht - der menschliche Verstand mag keine Veränderung 😉 ). Es funktioniert nur noch zum Teil. Wie Entwickler von Mickrigweich haben da echt ein kleines Meisterstück der Softwarearchitektur hingelegt, die sind leider nur sehr Gewöhnungsbedürftig.

Schau mal hier : http://www.wpftutorial.net/HowToCreateACustomControl.html

Dort findest du die Grundlagen für die Erstellung von CustomControls...

Die MSDN Hilfe wirst du auch benötigen. Ist leider immer nicht immer so gut nachzuvollziehen, aber dort befinden sich alle Informationen die du brauchst.

Wenn's zum weinen nicht reicht, lach drüber!

Quaneu Themenstarter:in
692 Beiträge seit 2008
vor 9 Jahren

Danke für Deine Geduld 😃

Mit Deinem Beispiel verstehe ich es auch aber für meinen Fall denke ich, dass ich es nicht anwenden kann.

Nehmen wir an, ich habe eine Liste mit n Elementen (ganz beliebige). Diese sollen in einer ListBox angezeigt werden. Dann würde ich es genau so machen wie Du.
ABER:
Die ListBox soll immer so hoch wie das Fenster sein und nur so viele Elemente zeichnen wie dargestellt werden können. D.h. doch ich muss wissen wie hoch jedes Element ist

=> Ich muss auf den Elementen folgendes aufrufen


listview.Children.Add(rowControl);
rowControl.ApplyTemplate();
rowControl.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));

Danach kann ich über die DesiredSize abfragen wie hoch das Element ist und somit weiß ich wie viele ich der ListView wirklich hinzufügen kann. Die anderen muss ich wieder entfernen, bevor die ListBox gezeichnet wird. Wie soll ich sonst herausfinden, wieviele von den n Elementen ich anzeigen soll? Das meinte ich mit "Virtualisierung" 😃.

Schöne Grüße
Quaneu

P
157 Beiträge seit 2014
vor 9 Jahren

Grins 😄 und woher weißt du wie hoch dein Fenster ist wenn du einmal die Größe deines Childs berechnest dürfte sich das nich ändern, der arme Anwender darf das Fenster nicht ändern..its a feature not a bug 😉

Also wenn du dein Control unbedingt dazu zwingen möchtest die Virtualisierung an und ab zu schalten, sind wir wieder beim Trigger 😉

Du hast doch ein ControlTemplate gemacht oder ? ... ich hoffe es jedenfalls...

Gib deinem Control eine neue Eigenschaft : **IchZwingeDirMeinenVirtualisierungsWillenAuf **... natürlich ein DependecyProperty (niemals INotficationChanged in CustomControls verwenden...dat gibt nur probleme beim Binden an das Control später)

Dann machst du dir nen Trigger gegen diese Eigenschaft und (de)aktivierst die Virtualisierung aufgrund dieser Eigenschaft.

So dann suchst du dir folgendes : OnRenderSizeChanged/OnRender oder irgendeine passende Methode die du überschreiben kannst.

Gibt viele OnMethoden die man verwenden kann...

        
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
        {
            base.OnRenderSizeChanged(sizeInfo);

            IchZwingeDirMeinenVirtualisierungsWillenAuf = JAWOLL/NEIIIN; --!! true/ false geht natürlich auch !!--
        }


Das wird aufgerufen wenn sich die Bounds deines Containers (Canvas) geändert haben. Von dort aus kannst du auf DesiredSize deines Childs zugreifen...ist aber n hack...

Dann setzte du **IchZwingeDirMeinenVirtualisierungsWillenAuf ** auf deinen Wert...

Und dieses

Wenn's zum weinen nicht reicht, lach drüber!

Quaneu Themenstarter:in
692 Beiträge seit 2008
vor 9 Jahren

???
Das Fenster ist mein CustomControl in diesem Beispiel und über SizeChanged() bekomme ich die Änderung mit. Wenn dieses Event gefeuert wird, "berechne" ich die Größe der Elemente neu. Wenn ich nun feststelle, das z.B. Elemente hinzugefügt werden können, dann füge ich diese hinzu. Die Virtualisierung will ich immer, da in der Matrix sehr sehr viele Elemente angezeigt werden können, 10000 wäre keine Ausnahme.

P
157 Beiträge seit 2014
vor 9 Jahren

Ich zitiere mich mal selbst:

Viele Entwickler denken noch in Pixeln und Events und UI-CodeBehind..

Benutz Events nur dann wenn du sie wirklich brauchst...die sind BÖSE. Damit kannst du jegliche Form von schwer zu debuggenden Fehlern verursachen. Einer der schlimmsten is : nicht vom Event deregistriert, aber objekt wurde irgendwo "gelöscht", also soll nicht mehr verwendet werden...und irgendwer löst den event aus und der listener deines gelöschten objekts arbeitet noch munter weiter...das sind so Probleme die kein Mensch braucht.
Anderer Fall : Event A Event B Event C Event A - Ein sehr häufiges Problem.

Es ist ziemlich ungünstig dass du dein Fenster als Customcontrol betitelst 😉 denn dann ist es keins. Ein CustomControl ist ein Steuerelement, sowas wie ne Textbox oder ein Label, nur mit eigener Implementierung..ein Fenster ist ein Fenster...auch wenns die gleichen Mechanismen sind, steckt eine andere "Idee" dahinter.

Versuch mal diesen Ansatz sonst wirds noch komplizierter:


    public class VirtualCanvas : Canvas
    {

        public bool UseVirtualize
        {
            get { return (bool)GetValue(UseVirtualizeProperty); }
            set { SetValue(UseVirtualizeProperty, value); }
        }
        public static readonly DependencyProperty UseVirtualizeProperty =
            DependencyProperty.Register("UseVirtualize", typeof(bool), typeof(VirtualCanvas), new PropertyMetadata(false));

        public VirtualCanvas()
        {
        }
        protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
        {
            base.OnRenderSizeChanged(sizeInfo);

            UseVirtualize = true;//false
        }
    }

In dem Link den ich dir geschickt habe, ist noch aufgezeigt wie du die für solch eine Lösung ein ControlTemplate erstellst. Ohne das wirst du wirklich nicht mehr weit kommen. Dein Ansatz der Lösung ist vermutlich recht weit weg von der Objekttrennung.

Du hattest doch ganz am Anfang geschrieben(Wenn nun aber nur m angezeigt werden können (m<n) so sollen auch nur m gezeichnet werden.)..da musst du nur die Virtualiserung einschalten, dann ist das Thema durch...in so fern das dein Panel unterstützt. Virtualizing-StackPanel googeln. Ist ja nich so dass es sowas schon gibt.

Kannst mal deinen Code packen und in die "cloud" schmeißen, dann schau ich beizeiten mal drüber.

vg

Wenn's zum weinen nicht reicht, lach drüber!

Quaneu Themenstarter:in
692 Beiträge seit 2008
vor 9 Jahren

Danke für deine eindringlichen Hinweise 😉. Ich kenn das Problem mit Events und benutzte sie sehr behutsam.

Ich glaub wir fangen nochmal bei 0 an. Das mit dem Fenster sollte ein Beispiel sein.

Also:

Ich habe MyCustomControl, dass ein CustomControl ist und von Control ableitet. Dieses Control verlangt ein Canvas im ControlTemplate.


[TemplatePart(Name = "PART_Canvas", Type = typeof(Canvas))]

In diesem Canvas will ich nun sehr sehr viele Elemente anzeigen lassen. Wenn ich sie alle zeichnen würde, würde es erstens lange dauern und das scrollen wäre auch nicht wirklich "schön". Daher will ich IMMER nur die Elemente dem Canvas hinzufügen und zeichnen, die in das Canvas passen. MyCustomControl hat ein Dependency Property an welches man sich binden kann, um so alle Items (noch keine UIElements) übergeben kann.
Wenn sich nun das Property ändert, oder die Größe des Canvas ändert, so muss ich die Größe aller Elemente (Children) berechnen, um zu bestimmen, welche nun wirklich dem Canvas hinzugefügt werden dürfen und welche eben nicht.
=> Mein Ziel ist es, dem Canvas nur soviele Children hinzuzufügen wie nötig.

Beispiel:
Ich habe 3456 Items die angezeigt werden sollen, aber es passen nur 30 in das Canvas, dann hat das Canvas nur 30 Children und der Rest "wartet" bis sie in den sichtbaren Bereich kommen.

Schöne Grüße
Qauneu

211 Beiträge seit 2008
vor 9 Jahren

Dann sind wir doch wieder bei dem Thema wo ich anfangs dachte wo du hinwillst?
Du wirst dennoch die "Datenliste" benötigen mit den 30000 Elementen - das kann dir auch egal sein du musst dich eben um die performante "Zeichnerei" der Elemente kümmern.

Das wäre dann ein VirtualiazingCanvas in dem Falle einfach?
Darüber haben sich schon Leute gedanken gemacht:
WPF Performance and .NET Framework Client Profile

Oder versteh ich dich nun wieder falsch?
Weil dann ist dein ItemsPanel kein "normales" Canvas sondern eben ein VirtualizingCanvas.

Kontakt & Blog: www.giesswein-apps.at

Quaneu Themenstarter:in
692 Beiträge seit 2008
vor 9 Jahren

@LatinChriz:
Ja es geht sehr stark in diese Richtung. Im Grunde ist es ein VirtualizingCanvas, das ein bisschen anders arbeiten würde.

Ich wollte nur sicher gehen, ob das auch der saubere Weg ist. Da es mir anfangs komisch vorkam, Elemente hinzufügen um Größe zu berechnen, dann wieder entfernen usw...

Aber anscheinend ist es der einzige und wohl auch richtige Weg.

P
157 Beiträge seit 2014
vor 9 Jahren

Naja, wer sagt dass du die Löschen musst, du willst die ja nur nich zeichnen...

Die Virtualisierung arbeitet so, dass wenn du viele viele Listenelemente hast, nur die gezeichnet werden die sichtbar sind.

Wenn du ne Listview verwendest, kannst du das auch so machen. Die hat eine Eigenschaft dafür oder zumindest eine angehängte.

Deine Anforderung klingt nicht nach viel, ist es aber im Endeffekt. Alles was mit Grafikk zu tun hat, hat, sagen wir mal ein anderes Kaliber als ne "doofe" Datenbank oder Web-anwendung. Dort liegen die Schwerpunkte meist in "hübscher" und/oder benutzbarer Darstellung und Datenhaltung.

Wenns um Optimierungszeugs geht, braucht man Abstraktionsvermögen und etwas Geduld, nicht jede Idee optimiert auch 😉

Dieser VirtualCanvas ist ganz gut und sollte für deine Zwecke voll ausreichen. Soweit ich das sehen arbeitet der mit einem einfachen Clippingalgorithmus über die Boundaries(das ist das rechteck das man gedanklich um ein objekt ziehen kann- also min/max koordinaten oder die größe), die werden mit der größe des Controls und der Größenabgabe berechnet (das matrizenzeugs musst du mal außer acht lassen, ist wohl nur für skalierung und verschiebung drin)

Es gibt in der VirtualCanvas Klasse eine Methode MeasureOverride() ... da sieht man recht gut wie das Measure funktioniert. Vielleicht hilf dir das etwas weiter...oder du benutzt das Teil einfach 😉
Die Klasse hat übrigens nur zufällig den gleichen Namen ... meine ist viel primitiver

vg

Wenn's zum weinen nicht reicht, lach drüber!