Свойства ExpandoObject с привязкой к данным не обновляются в динамическом XAML.C#

Место общения программистов C#
Anonymous
Свойства ExpandoObject с привязкой к данным не обновляются в динамическом XAML.

Сообщение Anonymous »

Я создаю приложение AvaloniaUI с помощью набора инструментов сообщества MVVM, где мне нужно динамически отображать элементы управления и привязывать их к данным модели представления, свойства которой будут неизвестны до момента выполнения. Фактически, я буду читать свойства и поля, к которым они должны быть привязаны, из файла JSON. В качестве доказательства концепции я написал это небольшое приложение для тестирования этих динамических привязок. Я пытаюсь использовать ExpandoObject для инкапсуляции тех динамических свойств, к которым я привязываюсь к динамически создаваемым элементам управления. К сожалению, ни одно динамически созданное свойство не обновляется в динамически создаваемых полях.
Динамические элементы управления будут добавлены как дочерние элементы StackPanel в пользовательский элемент управления: Его модель представления определяет несколько известных свойств:

Код: Выделить всё

using Avalonia.Media;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Collections.Generic;
using System.Dynamic;

namespace DynamicBindingControls.ViewModels;
public partial class DynamicSettingsVM : ViewModelBase { // ViewModelBase inherits from ObservableObject
[ObservableProperty]
private string _settingsGroupName;
[ObservableProperty]
private string _settingsGroupText;
[ObservableProperty]
private IBrush? _settingsGroupColor;
[ObservableProperty]
private bool _isBetterResolution;
public dynamic DynamicSettingsProperties { get; set; } = new ExpandoObject();

public DynamicSettingsVM(string defectName, string defectColor) {
SettingsGroupName = defectName;
SettingsGroupColor = new SolidColorBrush(Color.Parse(defectColor));
SettingsGroupText = "settings";
}

public void AddProperty(string propName, object initValue) {
var props = DynamicSettingsProperties as IDictionary;
props.Add(propName, initValue);
}

[RelayCommand]
public void ToggleResolution() {
if (DynamicSettingsProperties.Resolution == "1080p") {
DynamicSettingsProperties.Resolution = "4K";
IsBetterResolution = true;
} else {
DynamicSettingsProperties.Resolution = "1080p";
IsBetterResolution = false;
}
}
}
Внешний пользовательский элемент управления содержит этот динамически создаваемый пользовательский элемент управления: Выделенный код, где запускается динамическое создание:

Код: Выделить всё

using Avalonia.Controls;
using DynamicBindingControls.ViewModels;

namespace DynamicBindingControls.Views;

public partial class DisplaysDynamicControls : UserControl {
public DisplaysDynamicControls() {
InitializeComponent();
}

private void btnCreateDynamicControls_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) {
var dynaCtl = new DynamicSettings();
dynaCtl.DataContext = new DynamicSettingsVM("TV", "Orange");
dynaCtl.CreateTVSettings();
stpDynControls.Children.Add(dynaCtl);
}

private void btnCreateDynamicControlsRawDC_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) {
var dynaCtl = new DynamicSettings();
dynaCtl.CreateTVSettingsWithDynamicDC();
stpDynControls.Children.Add(dynaCtl);
}
}
Вернувшись во внутренний пользовательский элемент управления, он создает экземпляры собственных элементов управления и привязок и пробует два разных подхода:

Код: Выделить всё

using Avalonia;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Media;
using DynamicBindingControls.ViewModels;
using System.Dynamic;

namespace DynamicBindingControls.Views;

public partial class DynamicSettings :  UserControl {
public DynamicSettings() {
InitializeComponent();
}

public void CreateTVSettings() {
// Real app loads a lot of this info from a file
var myDC = DataContext as DynamicSettingsVM;
var brightnessSettingLabel = new TextBlock {
Text = "Brightness:",
Margin = new Thickness(5, 5)
};
var brightnessSettingField = new TextBox {
[!TextBox.TextProperty] = new Binding("DynamicSettingsProperties.Brightness")
};
var contrastSettingLabel = new TextBlock {
Text = "Contrast:",
Margin = new Thickness(5, 5)
};
var contrastSettingField = new TextBox {
[!TextBox.TextProperty] = new Binding("DynamicSettingsProperties.Contrast")
};
var channelSettingLabel = new TextBlock {
Text = "Channel:",
Margin = new Thickness(5, 5)
};
var channelSettingField = new TextBox {
[!TextBox.TextProperty] = new Binding("DynamicSettingsProperties.Channel")
};
var resolutionSettingLabel = new TextBlock {
Text = "Resolution:",
Margin = new Thickness(5, 5)
};
var resolutionSettingField = new TextBlock {
[!TextBlock.TextProperty] = new Binding("DynamicSettingsProperties.Resolution")
};
var sleepTimerOnBox = new CheckBox {
Content = "Sleep Timer On:",
[!CheckBox.IsCheckedProperty] = new Binding("DynamicSettingsProperties.IsSleepTimerOn")
};
var betterResolutionLabel = new TextBlock {
Text = "(better)",
[!TextBlock.IsVisibleProperty] = new Binding("IsBetterResolution", BindingMode.TwoWay),
Margin = new Thickness(5, 5)
};
var resToggler = new Button {
Content="Toggle Resolution",
[!Button.CommandProperty] = new Binding("ToggleResolutionCommand")
};
myDC.AddProperty("Brightness", 78);
myDC.AddProperty("Contrast", 54);
myDC.AddProperty("Channel", 24);
myDC.AddProperty("Resolution", "1080p");
myDC.AddProperty("IsSleepTimerOn", true);

stpSettingsGroups.Children.Add(new StackPanel {
Orientation = Avalonia.Layout.Orientation.Horizontal,
Children = { brightnessSettingLabel, brightnessSettingField }
});
stpSettingsGroups.Children.Add(new StackPanel {
Orientation = Avalonia.Layout.Orientation.Horizontal,
Children = { contrastSettingLabel, contrastSettingField }
});
stpSettingsGroups.Children.Add(new StackPanel {
Orientation = Avalonia.Layout.Orientation.Horizontal,
Children = { channelSettingLabel, channelSettingField }
});
stpSettingsGroups.Children.Add(new StackPanel {
Orientation = Avalonia.Layout.Orientation.Horizontal,
Children = { resolutionSettingLabel, resolutionSettingField, betterResolutionLabel }
});
stpSettingsGroups.Children.Add(sleepTimerOnBox);
stpSettingsGroups.Children.Add(resToggler);
}

public void CreateTVSettingsWithDynamicDC() {
dynamic myDC = new ExpandoObject();
myDC.SettingsGroupName = "Television";
myDC.SettingsGroupColor = new SolidColorBrush(Colors.Magenta);
myDC.SettingsGroupText = "settings";
myDC.Brightness = 78;
myDC.Contrast = 54;
myDC.Channel = 24;
myDC.Resolution = "1080p";
myDC.IsBetterResolution = false;
myDC.IsSleepTimerOn = true;
var brightnessSettingLabel = new TextBlock {
Text = "Brightness:",
Margin = new Thickness(5, 5)
};
var brightnessSettingField = new TextBox {
[!TextBox.TextProperty] = new Binding("Brightness")  // No "DynamicSettingsProperties."
};
var contrastSettingLabel = new TextBlock {
Text = "Contrast:",
Margin = new Thickness(5, 5)
};
var contrastSettingField = new TextBox {
[!TextBox.TextProperty] = new Binding("Contrast")
};
var channelSettingLabel = new TextBlock {
Text = "Channel:",
Margin = new Thickness(5, 5)
};
var channelSettingField = new TextBox {
[!TextBox.TextProperty] = new Binding("Channel")
};
var resolutionSettingLabel = new TextBlock {
Text = "Resolution:",
Margin = new Thickness(5,  5)
};
var resolutionSettingField = new TextBlock {
[!TextBlock.TextProperty] = new Binding("Resolution")
};
var sleepTimerOnBox = new CheckBox {
Content = "Sleep Timer On:",
[!CheckBox.IsCheckedProperty] = new Binding("IsSleepTimerOn")
};
var betterResolutionLabel = new TextBlock {
Text = "(better)",
[!TextBlock.IsVisibleProperty] = new Binding("IsBetterResolution", BindingMode.TwoWay),
Margin = new Thickness(5, 5)
};
DataContext = myDC;

stpSettingsGroups.Children.Add(new StackPanel {
Orientation = Avalonia.Layout.Orientation.Horizontal,
Children = { brightnessSettingLabel, brightnessSettingField }
});
stpSettingsGroups.Children.Add(new StackPanel {
Orientation = Avalonia.Layout.Orientation.Horizontal,
Children = { contrastSettingLabel, contrastSettingField }
});
stpSettingsGroups.Children.Add(new StackPanel {
Orientation = Avalonia.Layout.Orientation.Horizontal,
Children = { channelSettingLabel, channelSettingField }
});
stpSettingsGroups.Children.Add(new StackPanel {
Orientation = Avalonia.Layout.Orientation.Horizontal,
Children = { resolutionSettingLabel, resolutionSettingField, betterResolutionLabel }
});
stpSettingsGroups.Children.Add(sleepTimerOnBox);
}
}
Нажатие первой кнопки в верхней части окна создает экземпляр виртуальной машины для DataContext внутреннего элемента управления и добавляет свойства к ее ExpandoObject. Только другие свойства виртуальной машины демонстрируют правильное поведение, включая нажатие кнопки «Переключить», чтобы показать или скрыть «(лучше)»:
Изображение

Нажатие второй кнопки пытается привязать только ExpandoObject в качестве DataContext, к которому также добавлены некоторые свойства, которые явно ожидает XAML:
Изображение

Что я делаю неправильно и как заставить это работать? Мне не хватает какого-то механизма уведомлений? Спасибо…
EDIT 10 февраля 2026 г., 12:20 CST: я забыл упомянуть, что когда DynamicSettings.CreateTVSettings доходит до операторов stpSettingsGroups.Children.Add, в выводе отладки для каждого динамического свойства есть такая строка:

Код: Выделить всё

[Binding]An error occurred binding 'Text' to 'DynamicSettingsProperties.Brightness' at 'Brightness': 'Could not find a matching property accessor for 'Brightness' on 'System.Dynamic.ExpandoObject'.' (TextBox #59802499)
И все же, если я добавлю/добавлю следующий код в DynamicSettingsVM для этого:

Код: Выделить всё

 public DynamicSettingsVM(string defectName, string defectColor) {
SettingsGroupName = defectName;
SettingsGroupColor = new SolidColorBrush(Color.Parse(defectColor));
SettingsGroupText = "settings";
((INotifyPropertyChanged)DynamicSettingsProperties).PropertyChanged +=
new PropertyChangedEventHandler(HandlePropertyChanges);
}

private void HandlePropertyChanges(
object sender, PropertyChangedEventArgs e) {
var dict = DynamicSettingsProperties as IDictionary;
Trace.WriteLine($"{e.PropertyName} has changed to {dict[e.PropertyName]}.");
}
Когда я нажимаю кнопку «Переключить разрешение», он выводит что-то вроде этого:

Код: Выделить всё

Resolution has changed to 4K.
Хотя после первого нажатия кнопки пишет:

Код: Выделить всё

Exception thrown: 'Microsoft.CSharp.RuntimeBinder.RuntimeBinderException' in Microsoft.CSharp.dll
Этот бит «успеха» происходит только для разрешения, и текст не отображается в окне. Надеюсь, это ценные подсказки...

Вернуться в «C#»