Moin,
ich tue mir gerade schwer beim richtigen Einbinden eines Enum in eine ListView mittels MVVM. Die ListView wird vom ObservableCollection<Plugin> befüllt. Nach dem Start wird das Enum gelesen und richtig angezeigt, sowohl als Text als auch als Image via Converter.
Beim Laden und Entladen eines Plugins wird der Wert des Enum-Propertys (State) verändert. Die Veränderung wird aber nicht von der ListView registiert, aber sehr wohl von den Buttons, die jenach Value des Enums aktiviert und deaktiviert werden.
Auf dem Screenshot im Anhang sieht man es deutlicher, als ich es beschreiben kann. Das Plugin wird beim Start geladen und sowohl das Icon als auch der State sind richtig. Nachdem ich das Plugin entladen habe, reagierten die Buttons, aber nicht die ListView.
Google hilft mir leider nicht weiter, denn obwohl ich nach ListView suche, sind die meisten Treffer über Comboboxen.
Wie macht man das richtig?
XAML:
<Window x:Class="TestApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:local="clr-namespace:TestApp"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="150" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid
Grid.Column="0" Grid.Row="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Margin="0, 5, 0, 0">
<ListView
Name="PluginListView"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
ItemsSource="{Binding PluginList}"
SelectedItem="{Binding SelectedPlugin}"
SelectionMode="Single"
SizeChanged="PluginListView_OnSizeChanged" Loaded="PluginListView_OnLoaded">
<ListView.View>
<GridView>
<GridViewColumn Width="32">
<GridViewColumn.CellTemplate>
<DataTemplate>
<Image Tag="{Binding State}" Width="16" Height="16" Margin="0">
<Image.Style>
<Style TargetType="Image">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Tag, RelativeSource={RelativeSource Self}}" Value="Loaded">
<Setter Property="Source" Value="/Resources/green_icon.png"/>
</DataTrigger>
<DataTrigger Binding="{Binding Path=Tag, RelativeSource={RelativeSource Self}}" Value="Unloaded">
<Setter Property="Source" Value="/Resources/red_icon.png"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Image.Style>
</Image>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Path" DisplayMemberBinding="{Binding ProjectPath}" Width="Auto" />
<GridViewColumn Header="State" DisplayMemberBinding="{Binding State}" Width="80"/>
<GridViewColumn Header="Auto Load" Width="80">
<GridViewColumn.CellTemplate>
<DataTemplate>
<CheckBox
IsChecked="{Binding Path=AutoLoad, Mode=TwoWay}"
HorizontalAlignment="Center"
Checked="OnCheckboxCheckChange"
Unchecked="OnCheckboxCheckChange"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Auto Reload" Width="80">
<GridViewColumn.CellTemplate>
<DataTemplate>
<CheckBox
IsChecked="{Binding Path=AutoReload, Mode=TwoWay}"
HorizontalAlignment="Center"
Checked="OnCheckboxCheckChange"
Unchecked="OnCheckboxCheckChange"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
<TextBox
Text="{Binding GConsoleText}"
Name="GConsole"
VerticalAlignment="Bottom"
MinHeight="100"/>
</Grid>
<StackPanel Grid.Column="1">
<StackPanel Orientation="Horizontal" Margin="5">
<Button
Content="Add"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Padding="3"
Command="{Binding AddCommand}" />
<Button
Content="Remove"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Margin="10, 0, 0, 0"
Padding="3"
Command="{Binding RemoveCommand}" CommandParameter="Remove"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="5">
<Button
Content="Load"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Padding="3"
Command="{Binding LoadCommand}" CommandParameter="Load"/>
<Button
Content="Unload"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Margin="10, 0, 0, 0"
Padding="3"
Command="{Binding UnloadCommand}" CommandParameter="Unload"/>
</StackPanel>
</StackPanel>
</Grid>
</Grid>
</Window>
(Kann leider nicht mehr Code einfügen, da ich 8k Zeichen aufgebraucht sind.)
Hast du denn INotifyPropertyChanged
für die Klasse Plugin
korrekt implementiert?
Für das Plugin selbst nicht. Das Plugin ist in der ObservableCollection<Plugin> Klasse. Muss ich das noch für die Plugin-Klasse machen?
(Nur damit keine Verwirrung entsteht, ich habe inzwischen State in Status umbenannt.)
Plugin:
public class PluginList
{
public IList<Plugin> Plugins;
}
public class Plugin
{
/// <summary>
/// Represents the project path where the plugin is built.
/// </summary>
public string ProjectPath { get; set; }
/// <summary>
/// Determines if the plugin is loaded on start up.
/// </summary>
public bool AutoLoad { get; set; }
/// <summary>
/// Determines if the plugin is automaticly reloaded when changed in the project directory.
/// </summary>
public bool AutoReload { get; set; }
/// <summary>
/// Represents the current state of the plugin.
/// </summary>
[JsonIgnore] public PluginStatus Status { get; set; }
/// <summary>
/// Represents the absolute path (+file name) of the plugin in the application's plugin directory.
/// </summary>
[JsonIgnore] public string Path { get; set; }
/// <summary>
/// Instance of the AppDomain that was created for the plugin.
/// </summary>
[JsonIgnore] public AppDomain Domain { get; set; }
/// <summary>
/// Instance of the plugin.
/// </summary>
[JsonIgnore] public IPlugin Instance { get; set; }
}
public enum PluginStatus
{
Unloaded,
Initializing,
Loaded,
Disposing
}
ViewModel:
public class PluginViewModel : ViewModelBase
{
#region Properties
private ObservableCollection<Plugin> pluginList;
public ObservableCollection<Plugin> PluginList
{
get => pluginList;
set => SetProperty(ref pluginList, value);
}
private Plugin selectedPlugin;
public Plugin SelectedPlugin
{
get => selectedPlugin;
set => SetProperty(ref selectedPlugin, value);
}
private string gConsoleText;
public string GConsoleText
{
get => gConsoleText;
set => SetProperty(ref gConsoleText, value);
}
#endregion
#region ButtonCommands
private ICommand addCommand;
public ICommand AddCommand
{
get => addCommand ?? (addCommand = new CommandHandler(() => Add(), () => CanExecuteAdd));
}
public bool CanExecuteAdd
{
get => true;
}
private ICommand removeCommand;
public ICommand RemoveCommand
{
get => removeCommand ?? (removeCommand = new CommandHandler(() => Remove(), () => CanExecuteRemove));
}
public bool CanExecuteRemove
{
get => SelectedPlugin != null &&
SelectedPlugin.Status == PluginStatus.Unloaded;
}
private ICommand loadCommand;
public ICommand LoadCommand
{
get => loadCommand ?? (loadCommand = new CommandHandler(() => Load(), () => CanExecuteLoad));
}
public bool CanExecuteLoad
{
get => SelectedPlugin != null &&
SelectedPlugin.Status == PluginStatus.Unloaded;
}
private ICommand unloadCommand;
public ICommand UnloadCommand
{
get => unloadCommand ?? (unloadCommand = new CommandHandler(() => Unload(), () => CanExecuteUnload));
}
public bool CanExecuteUnload
{
get => SelectedPlugin != null &&
SelectedPlugin.Status == PluginStatus.Loaded;
}
#endregion
private string _pluginDir;
public PluginViewModel()
{
_pluginDir = Settings.PluginDir;
Manager.PluginManager.Instance.PluginLoaded += OnPluginLoaded;
Manager.PluginManager.Instance.PluginUnloaded += OnPluginUnloaded;
Manager.PluginManager.Instance.PluginRemoved += OnPluginRemoved;
this.pluginList = new ObservableCollection<Plugin>();
}
public void Add()
{
var filePath = OpenFileDialog();
if(string.IsNullOrEmpty(filePath))
return;
string pluginFile = Path.GetFileName(filePath);
if(IsAssemblyInPluginList(pluginFile))
{
ShowErrorDialog($"Plugin '{pluginFile}' is already in the List.");
return;
}
Plugin plugin = CreatePlugin(filePath);
try
{
Manager.PluginManager.Instance.Add(plugin);
}
catch(Exception e)
{
GConsole($"Error: {e.Message} ({filePath})");
ShowErrorDialog(e.Message);
return;
}
PluginList.Add(plugin);
Manager.PluginManager.Instance.Add(plugin);
SavePluginList();
}
public void Remove()
{
if(SelectedPlugin == null)
return;
Manager.PluginManager.Instance.Remove(SelectedPlugin);
PluginList.Remove(SelectedPlugin);
SavePluginList();
}
public void Load()
{
if(SelectedPlugin != null && SelectedPlugin.Status == PluginStatus.Unloaded)
try
{
Manager.PluginManager.Instance.Load(SelectedPlugin);
}
catch(TypeLoadException e)
{
string msg = $"'{Path.GetFileName(SelectedPlugin.Path)}' does not contain the class 'Plugin' in namespace '{Path.GetFileNameWithoutExtension(SelectedPlugin.Path)}'.";
ShowErrorDialog(msg);
GConsole(e.Message);
}
catch(SerializationException e)
{
string msg = $"'{Path.GetFileName(SelectedPlugin.Path)}' does not inherit from 'MarshalByRefObject' class.";
ShowErrorDialog(msg);
GConsole(e.Message);
}
}
public void Unload()
{
if(SelectedPlugin != null && SelectedPlugin.Status == PluginStatus.Loaded)
Manager.PluginManager.Instance.Unload(SelectedPlugin);
}
#region Helpers
//... Muss wieder Zeichen sparen.
}
Das steht in der Doku zu der ObservableCollection.
ObservableCollection
Um dynamische Bindungen einzurichten, bei denen die Benutzeroberfläche automatisch nach Einfügungen oder Löschungen in der Auflistung aktualisiert wird, muss die Auflistung die INotifyCollectionChanged-Schnittstelle implementieren. Diese Schnittstelle macht das CollectionChanged Ereignis verfügbar, ein Ereignis, das ausgelöst werden sollte, wenn sich die zugrunde liegende Auflistung ändert.
Was mir noch aufgefallen ist, aber ich bin absoluter Anfänger, warum hast du eine Klasse PluginList die eine IList<Plugin> hat und im ViewModel eine ObservableCollection<Plugin> PluginList.
Brauch man da die Klasse überhaupt?
Hallo Akanel,
dein zitierter Text beschreibt nur das allgemeine Vorgehen beim Binden von Daten, welches die ObservableCollection<T>
implementiert hat (wenn auch etwas eigenartig ausgedrückt).
Entscheidend ist der in pink gehaltene Text:
Hinweis
Um das Übertragen von Datenwerten von Bindungsquellobjekten an Bindungsziele vollständig zu unterstützen, muss jedes Objekt in Ihrer Auflistung, das bindungsfähige Eigenschaften unterstützt, einen entsprechenden Benachrichtigungsmechanismus für geänderte Eigenschaften wie die
INotifyPropertyChanged
Schnittstelle implementieren.
Es geht also darum, daß das Objekt (in diesem Fall also Plugin
) auch diese Schnittstelle implementiert haben muß - die ObservableCollection<T>
benachrichtigt nur beim Änderungen an der Auflistung selbst (Add
, Remove
, Clear
), nicht bei Änderungen der Eigenschaften der enthaltenen Objekte.
Was mir noch aufgefallen ist, aber ich bin absoluter Anfänger, warum hast du eine Klasse PluginList die eine IList<Plugin> hat und im ViewModel eine ObservableCollection<Plugin> PluginList.
Brauch man da die Klasse überhaupt?
Ja, ich brauche die Klasse. In der GUI hinzugefügte Plugins speichere ich in einer Json-Datei. IList<Plugin> wird beim Start vom Json's Deserializer mit Plugin-Objekten befüllt. Danach werden die Plugins ausgelesen, mit zusätzlichen Daten versehen und anschließend in die ObservableCollection<Plugin> eingetragen.
Es geht also darum, daß das Objekt (in diesem Fall also Plugin) auch diese Schnittstelle implementiert haben muß - die ObservableCollection<T> benachrichtigt nur beim Änderungen an der Auflistung selbst (Add, Remove, Clear), nicht bei Änderungen der Eigenschaften der enthaltenen Objekte.
Das war dann der Fehler. Ich bin davon ausgegangen, daß ObservableCollection auch die Objekte managet.
@PierreDole
Entschuldige, ich nahm an die Klasse gehört mit zum Problem.
@Th69
Vielen Dank für die Korrektur und Erläuterung des Abschnittes.
Hat mir auch wieder etwas Licht ins Dunkle gebracht. 🙂