Reputation: 22008
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
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:
IsValid
property to both the "implicit" DataTemplates.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
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
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
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