单视图,多视图模型 - 避免绑定错误
本文关键字:视图 模型 绑定 错误 单视图 | 更新日期: 2023-09-27 18:20:17
有一个视图(窗口(包含许多控件,以简化:
<!-- 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}" ... />
此视图用于显示和编辑多个配置:
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
对于某些配置,属性可能存在,也可能不存在。因此存在绑定错误。
问题:有没有办法隐藏当前Config
对象中不存在哪些属性的控件(这可以通过反射轻松完成(以及避免在视图中出现绑定错误(这是实际问题,我不想重新发明PropertyGrid
也不想使用一个(?
例如,如果Config = new ConfigType1()
(只有A
属性(,则View将只包含用于编辑属性A
的控件,用于编辑属性B
的控件,C
等应隐藏并且不会导致绑定错误。
如果有人愿意玩它,这是一个测试用例。
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;
}
}
最初存在缺少B
的绑定错误,点击按钮(ConfigB
将被分配(后出现缺少A
的绑定错误。
如何避免这些错误?可以通过检查反射来控制可见性是否存在属性(但仍然存在如何组织它的问题(。
你需要的是DataTemplate。
工作样品:
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; }
}
在我看来,tagaPdyk的答案是正确的,但我认为样本可以更好地解释如何做。
没有必要反思。这个想法是将隐式数据模板与内容演示器相结合。
假设我们必须数据类型:Data1
和Data2
。这是他们的代码:
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; }
}
现在我创建一个简单的视图模型:
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");
}
}
}
}
其中PropertyChangedBase
是 INotifyPropertyChanged
的基本实现类。
现在最重要的 - 对于这个问题 - 部分,即 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>
如您所见,我为要在ContentPresenter
中处理的每个对象定义了一个DataTemplate
。我必须为每个DataTemplate
设置DataType
属性。通过这种方式,正确的模板将自动在 ContentPresenter 中使用(取决于绑定到其 DataContext 的对象的类型(。
您可以使用"切换"按钮在Data1
对象和Data2
对象之间切换。此外,如果您查看 VS 的输出窗口,您将不会看到有关绑定错误的消息。
我希望我的示例可以帮助您解决问题。
编辑
我把答案的重点放在这样一个事实上,即使用数据模板,您不再有绑定错误。具有公共属性的对象和没有公共属性的对象之间没有太多区别。
无论如何,让我们假设Data1
和Data2
都派生自BaseData
类。这是它的简单代码:
public class BaseData
{
public bool IsValid { get; set; }
}
这样IsValid
是Data1
和Data2
的共同属性。现在,您可以在两种可能的解决方案之间进行选择:
- 将
IsValid
属性添加到两个"隐式"数据模板。 - 您创建一个"基本"数据模板(用于
BaseData
对象(,并在"隐式"数据模板中重用它(优点:您必须编写更少的 XAML - 缺点:它可能会影响 UI 性能(
关于第二个解决方案,您的数据模板将变为:
<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>
工作(但很糟糕(的解决方案是在代码隐藏中使用绑定:
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;
}
}
这里的关键是在配置更改时调用SetBindings()
,这将首先解绑(忽略DataContext
操作,它们只是因为缺少适当的 ViewModel,确保在取消绑定之前不要引发Config
更改事件!(,然后在代码隐藏中绑定一些反射检查,以避免绑定到不存在的属性以及控制可见性。
我将不得不使用这个解决方案,直到更好的解决方案到来......如果它来的话。
若要隐藏不想显示的控件,只需将 Visibility 属性(使用 BooleanToVisibilityConverter(绑定到主视图模型中的属性。
<TextBox Text="{Binding Config.B}" Visibility="{Binding ShowConfigB, Converter={StaticResource BooleanToVisibilityConverter}, Mode=OneWay}"/>
并在视图中模型
public bool ShowConfigB
{
get
{
return (Config.GetType() == typeof(ConfigType2));
}
}
我认为您不能仅使用 xaml 停止绑定错误。您可以在代码中添加或删除绑定,具体取决于使用 BindingOperations.SetBinding 和 BindingOperations.ClearBinding 使用的配置类。
请记住,用户不会看到绑定错误。只有当它们由于某种原因影响性能时,我才会担心它们。