c# - WPF - 如何与动态创建的控件实现双向数据绑定(bind)?

标签 c# wpf xaml reflection

我正在编写一个程序,该程序根据使用反射提取的属性的数据类型动态创建 Control。这是待检查主题的 View 。

    <ListView ItemsSource="{Binding PropertyControls}">
        <ListView.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal" Margin="8">
                    <TextBlock Text="{Binding PropertyName}" FontSize="14" Width="400"></TextBlock>
                    <UserControl FontSize="14" Content="{Binding Path=PropertyValue, Converter={StaticResource PropertyValueConverter}}"></UserControl>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>

我为 ListView 中的项目创建了一个项目模板。每行由两个元素组成;标签和动态创建的控件。

例如,如果 PropertyValue 是 bool 值,则动态创建的控件将是复选框。如果 PropertyValue 是字符串,则动态创建的控件将是 TextBox。如果 PropertyValue 是 FileInfo 列表,则将使用另一个 ListView 创建一个单独的窗口,并使用 OpenFileDialog 浏览按钮。

我能够通过创建一个实现 IValueConverter 的类来完成动态创建的控件,并按照 XAML 中的指定使用该类。 PropertyValueConverter 通过检查其数据类型将 PropertyValue 转换为动态创建的控件。

我的问题是,当选中复选框时,没有引发任何事件,并且 ViewModel 不会被其更改所修改。我怀疑是因为 XAML 中的绑定(bind)是针对 UserControl 而不是针对其子控件(恰好是 CheckBox)。虽然可以在 PropertyValueConverter 中以编程方式绑定(bind) IsChecked,但是有更好的方法来解决这个问题吗?

--------修订版 1 -------

public class PropertyControl: INotifyPropertyChanged
{
    public string PropertyName { get; set; }

    private object propertyValue;
    public object PropertyValue
    {
        get { return propertyValue; }
        set
        {
            propertyValue = value; 
            OnPropertyChanged(nameof(PropertyValue));
        }
    }

    #region INotifyPropertyChanged Implementation
    public event PropertyChangedEventHandler PropertyChanged;

    [NotifyPropertyChangedInvocator]
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
    #endregion
}

/// <summary>
/// Dynamically converts between value and control given a data type - control mapping.
/// </summary>
class PropertyValueConverter: IValueConverter
{
    /// <summary>
    /// Converts from value to control.
    /// </summary>
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (targetType == typeof (int))
            return new NumberTextBox {Text = value.ToString()};
        if (targetType == typeof (string))
            return new TextBox {Text = value.ToString()};
        if (targetType == typeof (bool))
            return new CheckBox {IsChecked = (bool) value};
        throw new Exception("Unknown targetType: " + targetType);
    }

    /// <summary>
    /// Converts from control to value.
    /// </summary>
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (targetType == typeof (NumberTextBox))
            return (value as NumberTextBox).Value;
        if (targetType == typeof(TextBox))
            return (value as TextBox).Text;
        if (targetType == typeof(CheckBox))
            return (value as CheckBox).IsChecked;
        throw new Exception("Unknown targetType: " + targetType);
    }
}

-------- 修订版 2 -------

public partial class SettingsWindow : Window
{
    public BindingList<SettingViewModel> ViewModels { get; set; }

    private SettingsManager settingsManager = new SettingsManager(new SettingsRepository());

    public SettingsWindow()
    {
        InitializeComponent();

        // Reloads the data stored in all setting instances from database if there's any.
        settingsManager.Reload();
        // Initialize setting view model.
        ViewModels = SettingViewModel.GetAll(settingsManager);
    }

    private void ResetButton_OnClick(object sender, RoutedEventArgs e)
    {
        settingsManager.Reload();
    }

    private void SaveButton_OnClick(object sender, RoutedEventArgs e)
    {
        settingsManager.SaveChanges();
    }
}

--- 选项卡控件 ---

<TabControl Name="ClassTabControl" TabStripPlacement="Left" ItemsSource="{Binding ViewModels}">
    <TabControl.Resources>
        <utilities:PropertyValueConverter x:Key="PropertyValueConverter" />
    </TabControl.Resources>
    <TabControl.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding DisplayName}" 
                       Margin="8" FontSize="14"></TextBlock>
        </DataTemplate>
    </TabControl.ItemTemplate>
    <TabControl.ContentTemplate>
        <DataTemplate>
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition/>
                    <RowDefinition Height="Auto"/>
                </Grid.RowDefinitions>
                <ListView ItemsSource="{Binding PropertyControls}">
                    <ListView.ItemTemplate>
                        <DataTemplate>
                            <StackPanel Orientation="Horizontal" Margin="8">
                                <TextBlock Text="{Binding PropertyName}" FontSize="14" Width="400"></TextBlock>
                                <CheckBox FontSize="14" IsChecked="{Binding Path=PropertyValue, Converter={StaticResource PropertyValueConverter}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"></CheckBox>
                            </StackPanel>
                        </DataTemplate>
                    </ListView.ItemTemplate>
                </ListView>
                <StackPanel Orientation="Horizontal" Grid.Row="1" Margin="8" HorizontalAlignment="Center">
                    <Button Name="ResetButton" Padding="4" Content="Reset" FontSize="14" Margin="4"
                            Click="ResetButton_OnClick"></Button>
                    <Button Name="SaveButton" Padding="4" Content="Save" FontSize="14" Margin="4"
                            Click="SaveButton_OnClick"></Button>
                </StackPanel>
            </Grid>
        </DataTemplate>
    </TabControl.ContentTemplate>
</TabControl>

最佳答案

更简单的方法是根据您的属性(property)类型创建模板。首先,您必须添加系统命名空间来访问所有基本类型:

xmlns:System="clr-namespace:System;assembly=mscorlib"

现在您可以摆脱转换器并在 XAML 中完成所有操作,如下所示:

<DataTemplate>
    <StackPanel x:Name="itemStackPanel" Orientation="Horizontal" Margin="8">
        <!-- General part -->
        <TextBlock Text="{Binding PropertyName}" FontSize="14" Width="400"/>
        <!-- Specific (property based) part -->
        <ContentPresenter Content="{Binding PropertyValue}">
            <ContentPresenter.Resources>
                <DataTemplate DataType="{x:Type System:String}">
                    <TextBlock Text="{Binding ElementName=itemStackPanel, Path=DataContext.PropertyValue}"/>
                </DataTemplate>
                <DataTemplate DataType="{x:Type System:Boolean}">
                    <CheckBox IsChecked="{Binding ElementName=itemStackPanel, Path=DataContext.PropertyValue}"/>
                </DataTemplate>
                <!-- ... -->
            </ContentPresenter.Resources>
        </ContentPresenter>
    </StackPanel>
</DataTemplate>

您只需根据需要为每种可能的类型创建一个模板即可。 ContentPresenter 根据 PropertyValue 的类型选择正确的模板。由于您要从模板外部绑定(bind)到父级,因此必须使用一个元素来绑定(bind) PropertyValue (如 Access parent DataContext from DataTemplate 中所述)。

关于c# - WPF - 如何与动态创建的控件实现双向数据绑定(bind)?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42666510/

相关文章:

wpf - 使用 Hashtable 作为 DataContext(用于 WPF/XAML 绑定(bind))

c# - 如何在 asp.net core 中设置小数位?

c# - 在wpf中设置画笔的颜色级别

c# - 从 RichTextBox 保存 RTF 时丢失表格宽度自动调整大小

c# - bool 转换器未命中转换器

c# - 设计 View Xaml 显示手机机身而不显示黑屏

c# - 从列表中选择值

c# - 为什么我需要将锁与信号量一起使用

c# - LINQ 在列表中查找所有具有 id 的内容

c# - wpf InteropBitmap 到位图