TreeView
Ein Baum, der nur die sichtbaren Zeilen anlegt. Aufgeklappt, angekreuzt und gesperrt stehen im Datenmodell und gelten auch ohne Zeile. Ein Kreuz gibt seinen Zustand an die Teile weiter, ein Elternknoten zeigt „unbestimmt“, wenn nur einige Teile angekreuzt sind. Das Regel-Ereignis ändert und sperrt weitere Knoten, bevor etwas geschrieben wird; hier schliessen sich „Minimal“ und „Vollständig“ aus, und der Kern ist gesperrt.


KXAML
TreeViewSeite.kxaml
<StackPanel xmlns="https://ui.kuonen.org/kxaml"
xmlns:x="https://ui.kuonen.org/kxaml/x"
x:Class="Kuonen.UI.Galerie.Seiten.TreeViewVorfuehrung"
Spacing="12">
<!-- Daten (Baum, Wurzel) setzt das Code-Behind; die Regeln laufen über die beiden Ereignisse. -->
<TreeView x:Name="Baum" Width="280" Height="220" SelectionMode="Extended"
CheckRegelnAnwenden="Baum_CheckRegelnAnwenden" CheckStatesChanged="Baum_CheckStatesChanged" />
<Button Content="Alles aufklappen" Click="Oeffnen_Click" />
<TextBlock x:Name="Meldung" Text="Noch nichts geändert." />
</StackPanel>TreeViewSeite.kxaml.cs
using System.ComponentModel;
using Kuonen.UI;
using Kuonen.UI.Bindung;
using Kuonen.UI.Controls;
using Kuonen.UI.Ereignisse;
namespace Kuonen.UI.Galerie.Seiten;
public sealed partial class TreeViewSeite
{
protected override Func<UIElement>? KxamlFabrik => () => new TreeViewVorfuehrung();
}
/// <summary>
/// KXAML-Fassung der TreeView-Seite: das Control steht in TreeViewSeite.kxaml; Datenmodell, Baumdaten und Regeln stehen
/// hier (für sich kopierbar, darum mit eigenem Modell <see cref="Teil"/>).
/// </summary>
public sealed partial class TreeViewVorfuehrung
{
private readonly Teil _programm, _minimal, _voll;
public TreeViewVorfuehrung()
{
InitializeComponent();
_programm = new Teil("Programm", null);
_ = new Teil("Kern (Pflicht)", _programm) { Haken = true, Sperre = "Der Kern wird immer installiert." };
_minimal = new Teil("Minimal", _programm);
_voll = new Teil("Vollständig", _programm);
foreach (var name in new[] { "Hilfe", "Beispiele", "Sprachpakete" }) _ = new Teil(name, _voll);
foreach (var sprache in new[] { "Deutsch", "Französisch", "Italienisch" }) _ = new Teil(sprache, _voll.Kinder[2]);
_programm.Haken = null;
Baum.Baum = new Baumdaten<Teil>(t => t.Kinder)
{
ElternVon = t => t.Eltern,
Aufgeklappt = BindingPath.Stufe<Teil, bool>(nameof(Teil.Offen), t => t.Offen, (t, w) => t.Offen = w),
Angekreuzt = BindingPath.Stufe<Teil, bool?>(nameof(Teil.Haken), t => t.Haken, (t, w) => t.Haken = w),
Sperre = BindingPath.Stufe<Teil, string?>(nameof(Teil.Sperre), t => t.Sperre, (t, w) => t.Sperre = w),
};
Baum.ItemsSource = new[] { _programm };
}
// Regel: „Minimal“ und „Vollständig“ schliessen sich aus (ausgewertet am geplanten Endzustand, vor dem Schreiben).
private void Baum_CheckRegelnAnwenden(object sender, CheckRegelArgs e)
{
bool minimalAn = EndzustandAn(e, _minimal), vollAn = EndzustandAn(e, _voll);
if (minimalAn && vollAn)
{
if (LiegtUnter(e.Knoten, _voll) && Baum.LiesHaken(_minimal) != false)
{
e.Abbrechen = true;
Meldung.Text = $"Abgelehnt: „{e.Knoten}“ gehört zu „Vollständig“, das sich mit „Minimal“ ausschliesst.";
return;
}
e.Setze(_voll, false);
vollAn = false;
}
if (minimalAn) e.Sperre(_voll, "Schliesst sich mit „Minimal“ aus.");
else e.Entsperre(_voll);
if (vollAn) e.Sperre(_minimal, "Schliesst sich mit „Vollständig“ aus.");
else e.Entsperre(_minimal);
}
// Nach dem Schreiben genau eine Meldung je Vorgang, mit dem Grund jeder neuen Sperre.
private void Baum_CheckStatesChanged(object sender, CheckStatesChangedArgs e)
{
var text = $"{e.Aenderungen.Count} Kästchen geändert, {e.GesperrtGeaendert.Count} Sperren geändert.";
foreach (var k in e.GesperrtGeaendert)
if (Baum.LiesSperre(k) is { } grund) text += $" „{k}“ gesperrt: {grund}";
Meldung.Text = text;
}
private void Oeffnen_Click(object sender, RoutedEventArgs e)
{
foreach (var t in _programm.Alle()) t.Offen = true;
}
/// <summary>Wahr, wenn der Knoten nach dem geplanten Vorgang angekreuzt oder unbestimmt wäre (sonst gilt der heutige Zustand).</summary>
private bool EndzustandAn(CheckRegelArgs e, Teil knoten)
{
bool? zustand = Baum.LiesHaken(knoten);
foreach (var (k, _, neu) in e.Geplant)
if (ReferenceEquals(k, knoten)) zustand = neu;
if (zustand != false) return true;
foreach (var (k, _, neu) in e.Geplant)
if (neu != false && !ReferenceEquals(k, knoten) && LiegtUnter(k, knoten)) return true; // ein Teil wird angekreuzt
return false;
}
/// <summary>Wahr, wenn <paramref name="knoten"/> der Knoten <paramref name="wurzel"/> selbst ist oder darunter liegt.</summary>
private static bool LiegtUnter(object knoten, Teil wurzel)
{
for (var t = knoten as Teil; t is not null; t = t.Eltern)
if (ReferenceEquals(t, wurzel)) return true;
return false;
}
/// <summary>Ein Knoten des Beispielbaums; meldet Änderungen über <see cref="INotifyPropertyChanged"/>, damit die Zeilen folgen.</summary>
private sealed class Teil : INotifyPropertyChanged
{
private bool _offen = true;
private bool? _haken = false;
private string? _sperre;
public Teil(string name, Teil? eltern)
{
Name = name;
Eltern = eltern;
eltern?.Kinder.Add(this);
}
public string Name { get; }
public Teil? Eltern { get; }
public List<Teil> Kinder { get; } = [];
public bool Offen { get => _offen; set => Setze(ref _offen, value, nameof(Offen)); }
public bool? Haken { get => _haken; set => Setze(ref _haken, value, nameof(Haken)); }
public string? Sperre { get => _sperre; set => Setze(ref _sperre, value, nameof(Sperre)); }
public event PropertyChangedEventHandler? PropertyChanged;
public override string ToString() => Name;
public IEnumerable<Teil> Alle()
{
yield return this;
foreach (var kind in Kinder)
foreach (var teil in kind.Alle()) yield return teil;
}
private void Setze<T>(ref T feld, T wert, string name)
{
if (EqualityComparer<T>.Default.Equals(feld, wert)) return;
feld = wert;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
}C#
TreeViewSeite.cs
using System.ComponentModel;
using Kuonen.UI;
using Kuonen.UI.Bindung;
using Kuonen.UI.Controls;
using Kuonen.UI.Layout;
namespace Kuonen.UI.Galerie.Seiten;
/// <summary>Galerie-Seite: <see cref="TreeView"/> mit Kontrollkästchen und Regel.</summary>
public sealed partial class TreeViewSeite : GalerieSeite
{
public override string Titel => "TreeView";
public override string Beschreibung =>
"Ein Baum, der nur die sichtbaren Zeilen anlegt. Aufgeklappt, angekreuzt und gesperrt stehen im Datenmodell und "
+ "gelten auch ohne Zeile. Ein Kreuz gibt seinen Zustand an die Teile weiter, ein Elternknoten zeigt „unbestimmt“, "
+ "wenn nur einige Teile angekreuzt sind. Das Regel-Ereignis ändert und sperrt weitere Knoten, bevor etwas geschrieben "
+ "wird; hier schliessen sich „Minimal“ und „Vollständig“ aus, und der Kern ist gesperrt.";
public override string Stichworte => "Baum Knoten Kontrollkästchen Haken IsChecked IsLocked gesperrt CheckRegelnAnwenden CheckStatesChanged Baumdaten Auswahl";
public override string Datei => nameof(TreeViewSeite);
public override UIElement Baue()
{
var programm = new Teil("Programm", null);
_ = new Teil("Kern (Pflicht)", programm) { Haken = true, Sperre = "Der Kern wird immer installiert." };
var minimal = new Teil("Minimal", programm);
var voll = new Teil("Vollständig", programm);
foreach (var name in new[] { "Hilfe", "Beispiele", "Sprachpakete" }) _ = new Teil(name, voll);
foreach (var sprache in new[] { "Deutsch", "Französisch", "Italienisch" }) _ = new Teil(sprache, voll.Kinder[2]);
programm.Haken = null;
var daten = new Baumdaten<Teil>(t => t.Kinder)
{
ElternVon = t => t.Eltern,
Aufgeklappt = BindingPath.Stufe<Teil, bool>(nameof(Teil.Offen), t => t.Offen, (t, w) => t.Offen = w),
Angekreuzt = BindingPath.Stufe<Teil, bool?>(nameof(Teil.Haken), t => t.Haken, (t, w) => t.Haken = w),
Sperre = BindingPath.Stufe<Teil, string?>(nameof(Teil.Sperre), t => t.Sperre, (t, w) => t.Sperre = w),
};
var meldung = new TextBlock { Text = "Noch nichts geändert." };
var baum = new TreeView { Width = 280, Height = 220, Baum = daten, ItemsSource = new[] { programm }, SelectionMode = SelectionMode.Extended };
// Regel: „Minimal“ und „Vollständig“ schliessen sich aus. Ausgewertet wird der geplante Endzustand (e.Geplant), nicht
// nur der Auslöser: ein Klick auf die Wurzel gibt beiden den Haken weiter, dann gilt „Minimal“. Ein Teil von
// „Vollständig“ lässt sich bei angekreuztem „Minimal“ nicht ankreuzen (Abbruch mit Grund). Die ausgeschlossene
// Variante ist gesperrt, solange die andere angekreuzt ist. Das Ereignis läuft vor dem Schreiben.
baum.CheckRegelnAnwenden += (_, e) =>
{
bool minimalAn = EndzustandAn(baum, e, minimal), vollAn = EndzustandAn(baum, e, voll);
if (minimalAn && vollAn)
{
if (LiegtUnter(e.Knoten, voll) && baum.LiesHaken(minimal) != false)
{
e.Abbrechen = true;
meldung.Text = $"Abgelehnt: „{e.Knoten}“ gehört zu „Vollständig“, das sich mit „Minimal“ ausschliesst.";
return;
}
e.Setze(voll, false);
vollAn = false;
}
if (minimalAn) e.Sperre(voll, "Schliesst sich mit „Minimal“ aus.");
else e.Entsperre(voll);
if (vollAn) e.Sperre(minimal, "Schliesst sich mit „Vollständig“ aus.");
else e.Entsperre(minimal);
};
// Nach dem Schreiben genau eine Meldung je Vorgang, mit dem Grund jeder neuen Sperre.
baum.CheckStatesChanged += (_, e) =>
{
var text = $"{e.Aenderungen.Count} Kästchen geändert, {e.GesperrtGeaendert.Count} Sperren geändert.";
foreach (var k in e.GesperrtGeaendert)
if (baum.LiesSperre(k) is { } grund) text += $" „{k}“ gesperrt: {grund}";
meldung.Text = text;
};
var oeffnen = new Button { Content = "Alles aufklappen" };
oeffnen.Click += (_, _) =>
{
foreach (var t in programm.Alle()) t.Offen = true;
};
var seite = new StackPanel { Spacing = 12 };
seite.Children.Add(baum);
seite.Children.Add(oeffnen);
seite.Children.Add(meldung);
return seite;
}
/// <summary>Wahr, wenn der Knoten nach dem geplanten Vorgang angekreuzt oder unbestimmt wäre (sonst gilt der heutige Zustand).</summary>
private static bool EndzustandAn(TreeView baum, CheckRegelArgs e, Teil knoten)
{
bool? zustand = baum.LiesHaken(knoten);
foreach (var (k, _, neu) in e.Geplant)
if (ReferenceEquals(k, knoten)) zustand = neu;
if (zustand != false) return true;
foreach (var (k, _, neu) in e.Geplant)
if (neu != false && !ReferenceEquals(k, knoten) && LiegtUnter(k, knoten)) return true; // ein Teil wird angekreuzt
return false;
}
/// <summary>Wahr, wenn <paramref name="knoten"/> der Knoten <paramref name="wurzel"/> selbst ist oder darunter liegt.</summary>
private static bool LiegtUnter(object knoten, Teil wurzel)
{
for (var t = knoten as Teil; t is not null; t = t.Eltern)
if (ReferenceEquals(t, wurzel)) return true;
return false;
}
/// <summary>Ein Knoten des Beispielbaums; meldet Änderungen über <see cref="INotifyPropertyChanged"/>, damit die Zeilen folgen.</summary>
private sealed class Teil : INotifyPropertyChanged
{
private bool _offen = true;
private bool? _haken = false;
private string? _sperre;
public Teil(string name, Teil? eltern)
{
Name = name;
Eltern = eltern;
eltern?.Kinder.Add(this);
}
public string Name { get; }
public Teil? Eltern { get; }
public List<Teil> Kinder { get; } = [];
public bool Offen { get => _offen; set => Setze(ref _offen, value, nameof(Offen)); }
public bool? Haken { get => _haken; set => Setze(ref _haken, value, nameof(Haken)); }
public string? Sperre { get => _sperre; set => Setze(ref _sperre, value, nameof(Sperre)); }
public event PropertyChangedEventHandler? PropertyChanged;
public override string ToString() => Name;
public IEnumerable<Teil> Alle()
{
yield return this;
foreach (var kind in Kinder)
foreach (var teil in kind.Alle()) yield return teil;
}
private void Setze<T>(ref T feld, T wert, string name)
{
if (EqualityComparer<T>.Default.Equals(feld, wert)) return;
feld = wert;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
}