WPF - 如何使用动态创建的控件实现双向数据绑定?

时间:2017-03-08 08:35:39

标签: c# wpf xaml reflection

我正在编写一个程序,它根据使用反射提取的属性的数据类型动态创建Control。以下是考试科目的观点。

    <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是布尔值,则动态创建的控件将是一个复选框。如果PropertyValue是一个字符串,那么动态创建的控件将是一个TextBox。如果PropertyValue是FileInfo的列表,那么将使用另一个带有OpenFileDialog的ListView和浏览按钮创建一个单独的窗口。

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

我的问题是当CheckBox被选中时,没有引发任何事件,并且ViewModel没有被其更改修改。我怀疑是因为XAML中的绑定是针对UserControl而不是其子控件,恰好是CheckBox。虽然可以在PropertyValueConverter中以编程方式绑定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();
    }
}

--- Tab Control ---

<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>

2 个答案:

答案 0 :(得分:3)

更简单的方法是根据您的媒体资源类型创建模板。首先,您必须添加系统命名空间才能访问所有基本类型:

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的类型选择正确的模板。由于您要从模板中绑定父级,因此必须使用元素绑定PropertyValue(在Access parent DataContext from DataTemplate中描述)。

答案 1 :(得分:2)

/编辑好吧,有人更快:/

这是示例(没有INotifyPropertyChanged,因为我不想写太多代码;))

public interface IViewModel
{
    string PropertyName { get; set; }
}

public class StringViewModel : IViewModel
{
    public string PropertyName { get; set; }
    public string Content { get; set; }
}

public class BooleanViewModel : IViewModel
{
    public string PropertyName { get; set; }
    public bool IsChecked { get; set; }
}

public class MainViewModel
{
    public ObservableCollection<IViewModel> ViewModels { get; set; }

    public MainViewModel()
    {
        ViewModels = new ObservableCollection<IViewModel>
        {
            new BooleanViewModel {PropertyName = "Bool", IsChecked = true },
            new StringViewModel {PropertyName = "String", Content = "My text"}
        };

    }
}

<Window x:Class="WpfApplication2.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:local="clr-namespace:WpfApplication2"
        mc:Ignorable="d"
        xmlns:viewModel="clr-namespace:WpfApplication2"
        Title="MainWindow">
    <Grid>
        <ListView ItemsSource="{Binding ViewModels}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal"
                                Margin="8">
                        <TextBlock Text="{Binding PropertyName}" />
                        <ContentControl FontSize="14" Content="{Binding .}">
                            <ContentControl.Resources>
                                <DataTemplate DataType="{x:Type viewModel:StringViewModel}">
                                    <TextBox Text="{Binding Content}" />
                                </DataTemplate>
                                <DataTemplate DataType="{x:Type viewModel:BooleanViewModel}">
                                    <CheckBox IsChecked="{Binding IsChecked}" />
                                </DataTemplate>
                            </ContentControl.Resources>
                        </ContentControl>
                    </StackPanel>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>
</Window>

enter image description here