2

I'm writing a program that dynamically creates Control based on the data type of the properties extracted using reflection. Here is the view in subject for examination.

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

I created an item template for the items in ListView. Each row consists of two elements; the label and the dynamically created control.

For instance, if the PropertyValue is a boolean, then the dynamically created control will be a checkbox. If the PropertyValue is a string, then the dynamically created control will be a TextBox. If the PropertyValue is a list of FileInfo, then a separate window will be created with another ListView and browse button with OpenFileDialog.

I was able to accomplish the dynamically created control by creating a class that implements IValueConverter and which is utilized as specified in the XAML. The PropertyValueConverter converts the PropertyValue into a dynamically created control by inspecting its data type.

My problem is when the CheckBox is checked, there was no event raised and the ViewModel is not modified by its changes. I suspect because the binding in the XAML was made to the UserControl and not to its child control which happens to be a CheckBox. Although it is possible to bind the IsChecked programmatically in the PropertyValueConverter, is there a better way to solve this?

------- Revision 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);
    }
}

------- Revision 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>
Xegara
  • 103
  • 1
  • 7
  • 18
  • This means your PropertyControls collections contains different types, can you show them? – Fruchtzwerg Mar 08 '17 at 08:41
  • I haven't made the mapping yet but I can make a draft. In a minute. :) – Xegara Mar 08 '17 at 08:42
  • I would suggest another, in my eyes easier way, to create the controls. You could use a ContentControl and DataTemplates for the different types of viewmodels you want to create a view for. One advantage ist that you dont need a converter and could define everything in the xaml code; bindings included. If you want me to I could write a short example. – Mighty Badaboom Mar 08 '17 at 08:51
  • @MightyBadaboom yes please. I'd like to see your short example. – Xegara Mar 08 '17 at 08:52
  • Have a look at my answer – Mighty Badaboom Mar 08 '17 at 09:09

2 Answers2

3

A much easier way is to create templates based on the type of your property. First of all you have to add the system namespace to access all the basic types:

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

Now you can get rid of your converter and do it all in XAML like:

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

You simply create a template for every possible type like you need it. The ContentPresenter selects the right template based on the type of PropertyValue. Since you are going to bind to a parent from out of your template you have to use a element to bind on PropertyValue (described in Access parent DataContext from DataTemplate).

Community
  • 1
  • 1
Fruchtzwerg
  • 10,999
  • 12
  • 40
  • 49
  • What does just {Binding} mean? I only know {Binding Something} – Xegara Mar 08 '17 at 09:00
  • 2
    The DataContext of the ContentPresenter is PropertyValue. If you want to create a CheckBox or a TextBlock presenting this value means you need to bind to the DataContext. {Binding} means you are bind to the DataContext of the parent which is your property this case. – Fruchtzwerg Mar 08 '17 at 09:02
  • Why does the ItemsSource not work in XAML in revision 2? But if I specified in the code-behind file the ClassTabControl.ItemsSource = ViewModels; It works. – Xegara Mar 08 '17 at 10:01
  • Not sure what you mean - can you explain the problem again? – Fruchtzwerg Mar 08 '17 at 10:06
  • Sorry. I noticed in both of your solutions, the ItemsSource of the ListView is bound to PropertyControls, where the PropertyControls is defined as a property in the code-behind file. Actually, the full view hierarchy includes a parent view of TabControl above every ListView children. So I guess it will be equivalent to , however, it does not work. I do not know why. But if I explicitly bound the TabControl.ItemsSource=ViewModels; in the code-behind file, it works. – Xegara Mar 08 '17 at 10:10
  • Is PropertyControls inside a ViewModel or inside the parant DataContext? If not you can try binding with {Binding Path=PropertyControls, RelativeSource={RelativeSource AncestorType={x:Type Window}}} – Fruchtzwerg Mar 08 '17 at 10:26
  • PropertyControls is inside a ViewModel :) – Xegara Mar 08 '17 at 10:29
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/137537/discussion-between-fruchtzwerg-and-xegara). – Fruchtzwerg Mar 08 '17 at 10:30
  • Binding in the CheckBox seems to have a problem. It throws an XamlParseException and says that two-way binding requires Path or XPath. – Xegara Mar 09 '17 at 12:11
  • Totally correct - I also fixed some problems (in my answer) with the two way binding of the properties by using the DataContext of the itemStackPanel (described in http://stackoverflow.com/questions/3404707/access-parent-datacontext-from-datatemplate) - This solution works now for me. – Fruchtzwerg Mar 09 '17 at 15:29
2

/edit Ok, some one was faster :/

Here's the example (without INotifyPropertyChanged because I did not want to write too much code ;))

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

Mighty Badaboom
  • 6,067
  • 5
  • 34
  • 51
  • Good solution but remember: There is only one ViewModel (PropertyControl) and not different ones (BooleanViewModel, StringViewModel) like you described it. – Fruchtzwerg Mar 08 '17 at 09:10
  • Good hint, read it but forgot about it. Maybe Xegara want's to change this to become more typesafe. If not your DataTypes are a good idea. – Mighty Badaboom Mar 08 '17 at 09:18
  • Both of your solutions are interesting. I'll try the codes first. Thanks! – Xegara Mar 08 '17 at 09:36