Sinatr
Sinatr

Reputation: 22008

Single View, multiple ViewModels - avoid binding errors?

There is a single View (window) with many controls, to simplify:

<!-- edit property A -->
<TextBlock Text="A" ... />
<TextBox Text="{Binding Config.A}" ... />
<Button Command={Binding DoSometingWitA} ... />

<!-- edit property B -->
<TextBox Text="{Binding Config.B}" ... />

<!-- edit property C -->
<ComboBox Text="{Binding Config.C}" ... />

This View is used to display and edit multiple configurations:

public class ViewModel: INotifyPropertyChanged
{
    public BaseConfig Config {get {...} set {...}}
}

public class ConfigType1: BaseConfig { ... } // only has A
public class ConfigType2: BaseConfig { ... } // only has B
public class ConfigType3: BaseConfig { ... } // only has C
public class ConfigType4: BaseConfig { ... } // has A and B
public class ConfigType5: BaseConfig { ... } // has A and C

Properties may or may not exists for certain configuration. As a result there are binding errors.

Question: is there a way to hide controls which properties aren't present in current Config object (this could be easily done with reflection) as well as avoid having binding errors (and this is the actual problem, I don't want to re-invent PropertyGrid nor I want to use one) in the View?

E.g. if Config = new ConfigType1() (which has only A property), then View will only contains controls to edit property A, controls to edit property B, C, etc. should be hidden and do not cause binding errors.


Here is a test case if someone willing to play with it.

XAML:

<TextBox Text="{Binding Config.A}" Visibility="Collapsed"/>
<TextBox Text="{Binding Config.B}" Visibility="Hidden"/>
<Button VerticalAlignment="Bottom"
        Content="..."
        Click="Button_Click" />

CS:

public partial class MainWindow : Window
{
    public class BaseConfig { }

    public class ConfigA : BaseConfig
    {
        public string A { get; set; }
    }

    public class ConfigB : BaseConfig
    {
        public string B { get; set; }
    }

    public BaseConfig Config { get; private set; }

    public MainWindow()
    {
        InitializeComponent();
        Config = new ConfigA() { A = "aaa" };
        DataContext = this;
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        Config = new ConfigB() { B = "bbb" };
        DataContext = null;
        DataContext = this;
    }
}

Initially there is a binding error of missing B, after clicking button (ConfigB will be assigned) there is a binding error of missing A.

How to avoid those errors? Visibility can be controlled by checking with reflection if property exists (but there is still a question of how to organize that).

Upvotes: 1

Views: 499

Answers (4)

Il Vic
Il Vic

Reputation: 5666

In my opinion tagaPdyk's answer is correct, but I think a sample can explain better how to do.

There is no need of reflection. The idea is to combine implicit DataTemplates with a ContentPresenter.

Let's suppose we have to types of data: Data1 and Data2. Here their code:

public class Data1
{
    public string Name { get; set; }
    public string Description { get; set; }
}

public class Data2
{
    public string Alias { get; set; }
    public Color Color { get; set; }
}

Now I create a simple ViewModel:

public class ViewModel : PropertyChangedBase
{
    private Data1 data1 = new Data1();
    private Data2 data2 = new Data2();

    private object current;
    private RelayCommand switchCommand;

    public ViewModel1()
    {
        switchCommand = new RelayCommand(() => Switch());
        Current = data1;
    }

    public ICommand SwitchCommand
    {
        get
        {
            return switchCommand;
        }
    }

    public IEnumerable<Color> Colors
    {
        get
        {
            List<Color> colors = new List<Color>();
            colors.Add(System.Windows.Media.Colors.Red);
            colors.Add(System.Windows.Media.Colors.Yellow);
            colors.Add(System.Windows.Media.Colors.Green);

            return colors;
        }
    }

    private void Switch()
    {
        if (Current is Data1)
        {
            Current = data2;
            return;
        }

        Current = data1;
    }

    public object Current
    {
        get
        {
            return current;
        }
        set
        {
            if (current != value)
            {
                current = value;
                NotifyOfPropertyChange("Current");
            }
        }
    }
}

where PropertyChangedBase is a base implementation class of INotifyPropertyChanged.

Now the most important - for this question - part, i.e. the XAML

<Window x:Class="WpfApplication1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApplication1"
        Title="Window1" Height="300" Width="300">
    <Window.Resources>
        <CollectionViewSource x:Key="colors" Source="{Binding Path=Colors, Mode=OneTime}" />

        <DataTemplate DataType="{x:Type local:Data1}">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>

                <TextBlock Text="Name" VerticalAlignment="Center" />
                <TextBox Text="{Binding Name}" Grid.Column="1" Margin="5" />

                <TextBlock Text="Description" Grid.Row="1" VerticalAlignment="Center" />
                <TextBox Text="{Binding Description}" Grid.Column="1" Grid.Row="1" Margin="5" />
            </Grid>
        </DataTemplate>
        <DataTemplate DataType="{x:Type local:Data2}">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>

                <TextBlock Text="Alias" VerticalAlignment="Center" />
                <TextBox Text="{Binding Alias}" Grid.Column="1" Margin="5" />

                <TextBlock Text="Color" Grid.Row="1" VerticalAlignment="Center" />
                <ComboBox Text="{Binding Color}" Grid.Column="1" Grid.Row="1" Margin="5"
                          ItemsSource="{Binding Source={StaticResource colors}}" />
            </Grid>
        </DataTemplate>
    </Window.Resources>
    <StackPanel>
        <ContentPresenter Content="{Binding Path=Current}" />
        <Button Content="Switch" Command="{Binding SwitchCommand}" Margin="30" />
    </StackPanel>
</Window>

As you can see I define a DataTemplate for each object I want to handle in the ContentPresenter. I have to set the DataType property for each DataTemplate. In this way the proper template will be automatically used inside the ContentPresenter (depending on the type of the object binded to its DataContext).

You can use the "Switch" button to switch between a Data1 object and a Data2 one. Moreover if you look at the Output Window of your VS, you will see no messages about binding errors.

I hope my sample can help with your issue.

EDIT

I placed the emphasis of my answer on the fact that with DataTemplates you have binding errors no more. There are not so many differences between objects with common properties and objects without them.

Anyway, let's suppose that both Data1 and Data2 derive from BaseData class. Here its simple code:

public class BaseData
{
    public bool IsValid { get; set; }
}

In this way IsValid is a common property for Data1 and Data2. Now you can choose between two possible solutions:

  1. You add IsValid property to both the "implicit" DataTemplates.
  2. You create a "base" DataTemplate (for BaseData objects) and the you reuse it in the "implicit" DataTemplates (pro: you have to write less XAML - cons: it can have an impact on the UI performance)

Regarding the second solution, your DataTemplates will become:

<Window.Resources>
    <CollectionViewSource x:Key="colors" Source="{Binding Path=Colors, Mode=OneTime}" />

    <DataTemplate x:Key="{x:Type local:BaseData}">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="80" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>

            <TextBlock Text="Is valid" VerticalAlignment="Center" />
            <CheckBox IsChecked="{Binding IsValid}" Margin="5" Grid.Column="1" />
        </Grid>
    </DataTemplate>

    <DataTemplate DataType="{x:Type local:Data1}">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="80" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>

            <TextBlock Text="Name" VerticalAlignment="Center" />
            <TextBox Text="{Binding Name}" Grid.Column="1" Margin="5" />

            <TextBlock Text="Description" Grid.Row="1" VerticalAlignment="Center" />
            <TextBox Text="{Binding Description}" Grid.Column="1" Grid.Row="1" Margin="5" />

            <ContentPresenter Grid.Row="2" Grid.ColumnSpan="2" 
                                ContentTemplate="{StaticResource {x:Type local:BaseData}}" />
        </Grid>
    </DataTemplate>
    <DataTemplate DataType="{x:Type local:Data2}">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="80" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>

            <TextBlock Text="Alias" VerticalAlignment="Center" />
            <TextBox Text="{Binding Alias}" Grid.Column="1" Margin="5" />

            <TextBlock Text="Color" Grid.Row="1" VerticalAlignment="Center" />
            <ComboBox Text="{Binding Color}" Grid.Column="1" Grid.Row="1" Margin="5"
                        ItemsSource="{Binding Source={StaticResource colors}}" />

            <ContentPresenter Grid.Row="2" Grid.ColumnSpan="2" 
                                ContentTemplate="{StaticResource {x:Type local:BaseData}}" />
        </Grid>
    </DataTemplate>
</Window.Resources>

Upvotes: 1

tgpdyk
tgpdyk

Reputation: 1233

What you need is DataTemplate.

Working sample:

 public BaseConfig Config { get; set; }
 <Window.Resources>
    <DataTemplate DataType="{x:Type o:ConfigA}">
        <!--
          You can add here any control you wish applicable to ConfigA.
          Say, a textbox can do.  
         -->
        <TextBlock Text="{Binding A}"/>
    </DataTemplate>
    <DataTemplate DataType="{x:Type o:ConfigB}">
        <TextBlock Text="{Binding B}"/>
    </DataTemplate>
    <DataTemplate DataType="{x:Type o:ConfigType10000000000}">
        <superComplicatedControl:UniqueControl ProprietaryProperty="{Binding CustomProperty}"/>
    </DataTemplate>
    <!--  Rachel's point  -->
    <DataTemplate DataType="{x:Type o:Config4}">
        <StackPanel>
           <ContentControl Content="{Binding ConfigA}"/>
           <ContentControl Content="{Binding ConfigB}"/>
        </StackPanel>
    </DataTemplate>
</Window.Resources>
<Grid>
    <StackPanel>
         <ContentControl Content="{Binding Config}" />
         <Button VerticalAlignment="Bottom" Content="woosh" Click="Button_Click" />
    </StackPanel>
 </Grid>
private void Button_Click(object sender, RoutedEventArgs e)
{
    // Config = new ConfigB() { B = "bbb" };
    Config = new Config4() { ConfigA = (ConfigA) Config, ConfigB = new ConfigB { B = "bbb" } };
    DataContext = null;
    DataContext = this;
}

//…

// Rachel's point
public class Config4 : BaseConfig
{
    public string A4 { get; set; }

    public ConfigA ConfigA { get; set; }
    public ConfigB ConfigB { get; set; } 
}

Upvotes: 1

Sinatr
Sinatr

Reputation: 22008

Working (but a crappy) solution is to use binding in code-behind:

XAML:

<TextBox x:Name="textA" />
<TextBox x:Name="textB" />

CS:

public partial class MainWindow : Window
{

    ...

    void SetBindings()
    {
        BindingOperations.ClearAllBindings(textA);
        BindingOperations.ClearAllBindings(textB);
        DataContext = null;
        Bind(textA, "A");
        Bind(textB, "B");
        DataContext = this;
    }

    void Bind(UIElement element, string name)
    {
        if (Config?.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance) != null)
        {
            BindingOperations.SetBinding(element, TextBox.TextProperty, new Binding("Config." + name));
            element.Visibility = Visibility.Visible;
        }
        else
            element.Visibility = Visibility.Collapsed;
    }
}

Key here is to call SetBindings() whenever config is changed, which will first unbind (ignore DataContext manipulations, they are only here because of lacking proper ViewModel, make sure do not rise Config changed event until you unbind!) and then bind in code-behind with some reflection check to avoid binding to non-existing properties as well as control visibility.

I'll have to use this solution until better one comes.. if it ever comes.

Upvotes: 0

ajg
ajg

Reputation: 1753

To hide the controls you dont wnat to show you just need to bind the Visibility property (using a BooleanToVisibilityConverter) to properties in your main view model.

<TextBox Text="{Binding Config.B}" Visibility="{Binding ShowConfigB, Converter={StaticResource BooleanToVisibilityConverter}, Mode=OneWay}"/>

and in the ViewModel

public bool ShowConfigB
{
    get
    {
      return (Config.GetType() == typeof(ConfigType2));
    }
}

I don't think you can stop the binding errors with xaml only though. You could add or remove the bindings in code depending on the config class being used using BindingOperations.SetBinding and BindingOperations.ClearBinding.

Remember the users do not see the binding errors though. I'd only be worried about them if they were affecting performance for some reason.

Upvotes: -1

Related Questions