仅在存在的情况下绑定到属性 [英] Bind to property only if it exists

查看:56
本文介绍了仅在存在的情况下绑定到属性的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个WPF窗口,该窗口使用多个viewmodel对象作为其DataContext.窗口具有绑定到仅在某些viewmodel对象中存在的属性的控件.如果属性存在(并且仅存在),我如何绑定到该属性.

I have a WPF window that uses multiple viewmodel objects as its DataContext. The window has a control that binds to a property that exists only in some of the viewmodel objects. How can I bind to the property if it exists (and only if it exists).

我知道以下问题/答案: MVVM-当不存在绑定属性时隐藏控件.这行得通,但是给了我一个警告.可以在没有警告的情况下完成吗?

I am aware of the following question/answer: MVVM - hiding a control when bound property is not present. This works, but gives me a warning. Can it be done without the warning?

谢谢!

一些示例代码:

Xaml:

<Window x:Class="WpfApplication1.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfApplication1"
    Title="MainWindow" Height="350" Width="525">
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="40"/>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="40"/>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
    <ListBox Grid.Row="1" Name="ListView" Margin="25,0,25,0" ItemsSource="{Binding Path=Lst}"
              HorizontalContentAlignment="Center" SelectionChanged="Lst_SelectionChanged">
    </ListBox>
    <local:SubControl Grid.Row="3" x:Name="subControl" DataContext="{Binding Path=SelectedVM}"/>
</Grid>

SubControl Xaml:

SubControl Xaml:

<UserControl x:Class="WpfApplication1.SubControl"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         xmlns:local="clr-namespace:WpfApplication1"
         mc:Ignorable="d" 
         d:DesignHeight="200" d:DesignWidth="300">
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="40"/>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="40"/>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
    <StackPanel Grid.Row="1" Orientation ="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,5,0,5">
        <TextBlock Margin="5,0,5,0" Text="{Binding Path=OnOffAlways}"/>
        <CheckBox IsChecked="{Binding Path=Always}">
            <TextBlock Text="On/Off"/>
        </CheckBox>
    </StackPanel>
    <StackPanel Grid.Row="3" Orientation ="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,5,0,5">
        <TextBlock Margin="5,0,5,0" Text="{Binding Path=OnOffSometimes}"/>
        <CheckBox IsChecked="{Binding Path=Sometimes}">
            <TextBlock Text="On/Off"/>
        </CheckBox>
    </StackPanel>
</Grid>

后面的MainWindow代码:

MainWindow Code Behind:

    public partial class MainWindow : Window
{
    ViewModel1 vm1;
    ViewModel2 vm2;
    MainViewModel mvm;

    public MainWindow()
    {

        InitializeComponent();

        vm1 = new ViewModel1();
        vm2 = new ViewModel2();
        mvm = new MainViewModel();
        mvm.SelectedVM = vm1;
        DataContext = mvm;
    }

    private void Lst_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        ListBox lstBx = sender as ListBox;

        if (lstBx != null)
        {
            if (lstBx.SelectedItem.Equals("VM 1"))
                mvm.SelectedVM = vm1;
            else if (lstBx.SelectedItem.Equals("VM 2"))
                mvm.SelectedVM = vm2;
        }
    }
}

MainViewModel(MainWindow的DataContext):

MainViewModel (DataContext of MainWindow):

    public class MainViewModel : INotifyPropertyChanged
{
    ObservableCollection<string> lst;
    ViewModelBase selectedVM;

    public event PropertyChangedEventHandler PropertyChanged;

    public MainViewModel()
    {

        Lst = new ObservableCollection<string>();
        Lst.Add("VM 1");
        Lst.Add("VM 2");
    }

    public ObservableCollection<string> Lst
    {
        get { return lst; }
        set
        {
            lst = value;
            OnPropertyChanged("Lst");
        }
    }


    public ViewModelBase SelectedVM
    {
        get { return selectedVM; }
        set
        {
            if (selectedVM != value)
            {
                selectedVM = value;
                OnPropertyChanged("SelectedVM");
            }
        }
    }
    protected void OnPropertyChanged(string name)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(name));
        }
    }
}

ViewModel1(有时带有属性):

ViewModel1 (with sometimes property):

    public class ViewModel1 : ViewModelBase, INotifyPropertyChanged
{
    private bool _always;
    private string _onOffAlways;

    private bool _sometimes;
    private string _onOffSometimes;

    public event PropertyChangedEventHandler PropertyChanged;

    public ViewModel1()
    {
        _always = false;
        _onOffAlways = "Always Off";

        _sometimes = false;
        _onOffSometimes = "Sometimes Off";
    }

    public bool Always
    {
        get { return _always; }
        set
        {
            _always = value;
            if (_always)
                OnOffAlways = "Always On";
            else
                OnOffAlways = "Always Off";
            OnPropertyChanged("Always");
        }
    }

    public string OnOffAlways
    {
        get { return _onOffAlways; }
        set
        {
            _onOffAlways = value;
            OnPropertyChanged("OnOffAlways");
        }
    }

    public bool Sometimes
    {
        get { return _sometimes; }
        set
        {
            _sometimes = value;
            if (_sometimes)
                OnOffSometimes = "Sometimes On";
            else
                OnOffSometimes = "Sometimes Off";
            OnPropertyChanged("Sometimes");
        }
    }

    public string OnOffSometimes
    {
        get { return _onOffSometimes; }
        set
        {
            _onOffSometimes = value;
            OnPropertyChanged("OnOffSometimes");
        }
    }

    protected void OnPropertyChanged(string name)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(name));
        }
    }
}

ViewModel2(有时没有属性):

ViewModel2 (without Sometimes property):

    public class ViewModel2 : ViewModelBase, INotifyPropertyChanged
{
    private bool _always;
    private string _onOffAlways;

    public event PropertyChangedEventHandler PropertyChanged;

    public ViewModel2()
    {
        _always = false;
        _onOffAlways = "Always Off";
    }

    public bool Always
    {
        get { return _always; }
        set
        {
            _always = value;
            if (_always)
                OnOffAlways = "Always On";
            else
                OnOffAlways = "Always Off";
            OnPropertyChanged("Always");
        }
    }

    public string OnOffAlways
    {
        get { return _onOffAlways; }
        set
        {
            _onOffAlways = value;
            OnPropertyChanged("OnOffAlways");
        }
    }

    protected void OnPropertyChanged(string name)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(name));
        }
    }
}

public class AlwaysVisibleConverter : IValueConverter
{
    #region Implementation of IValueConverter

    public object Convert(object value,
                          Type targetType, object parameter, CultureInfo culture)
    {
        return Visibility.Visible;
    }

    public object ConvertBack(object value, Type targetType,
                              object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }

    #endregion
}

推荐答案

有多种方法可以解决您的情况.就其价值而言,您已经拥有的解决方案对我来说似乎很合理.您得到的警告(我想您是在谈论输出到调试控制台的错误消息)是相当无害的.它确实暗示了潜在的性能问题,因为它表明WPF正在从意外状况中恢复.但是我希望仅在视图模型发生更改时才产生成本,这应该足够频繁才有意义.

There are many different ways one could approach your scenario. For what it's worth, the solution you already have seems reasonable to me. The warning you get (I presume you are talking about the error message output to the debug console) is reasonably harmless. It does imply a potential performance issue, as it indicates WPF is recovering from an unexpected condition. But I would expect the cost to be incurred only when the view model changes, which should not be frequent enough to matter.

另一个选项(恕我直言是首选)是仅使用常规的WPF数据模板功能.也就是说,为您期望的每个视图模型定义一个不同的模板,然后让WPF根据当前视图模型选择合适的模板.看起来像这样:

Another option, which is IMHO the preferred one, is to just use the usual WPF data templating features. That is, define a different template for each view model you expect, and then let WPF pick the right one according to the current view model. That would look something like this:

<UserControl x:Class="TestSO46736914MissingProperty.UserControl1"
             x:ClassModifier="internal"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:l="clr-namespace:TestSO46736914MissingProperty"
             mc:Ignorable="d" 
             Content="{Binding}"
             d:DesignHeight="300" d:DesignWidth="300">
  <UserControl.Resources>
    <DataTemplate DataType="{x:Type l:ViewModel1}">
      <Grid>
        <Grid.RowDefinitions>
          <RowDefinition Height="40"/>
          <RowDefinition Height="Auto"/>
          <RowDefinition Height="40"/>
          <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <StackPanel Grid.Row="1" Orientation ="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,5,0,5">
          <TextBlock Margin="5,0,5,0" Text="{Binding Path=OnOffAlways}"/>
          <CheckBox IsChecked="{Binding Path=Always}">
            <TextBlock Text="On/Off"/>
          </CheckBox>
        </StackPanel>
        <StackPanel Grid.Row="3" Orientation ="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,5,0,5">
          <TextBlock Margin="5,0,5,0" Text="{Binding Path=OnOffSometimes}"/>
          <CheckBox IsChecked="{Binding Path=Sometimes}">
            <TextBlock Text="On/Off"/>
          </CheckBox>
        </StackPanel>
      </Grid>
    </DataTemplate>
    <DataTemplate DataType="{x:Type l:ViewModel2}">
      <Grid>
        <Grid.RowDefinitions>
          <RowDefinition Height="40"/>
          <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <StackPanel Grid.Row="1" Orientation ="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,5,0,5">
          <TextBlock Margin="5,0,5,0" Text="{Binding Path=OnOffAlways}"/>
          <CheckBox IsChecked="{Binding Path=Always}">
            <TextBlock Text="On/Off"/>
          </CheckBox>
        </StackPanel>
      </Grid>
    </DataTemplate>
  </UserControl.Resources>
</UserControl>

即只需将您的 UserControl 对象的 Content 设置为视图模型对象本身,以便使用适当的模板在控件中显示数据.没有该属性的视图模型对象的模板没有引用该属性,因此不会生成警告.

I.e. just set the Content of your UserControl object to the view model object itself, so that the appropriate template is used to display the data in the control. The template for the view model object that doesn't have the property, doesn't reference that property and so no warning is generated.

另一个与上面类似的选项也可以解决您对显示的警告的担忧,它是创建一个垫片"(又称适配器")对象,该对象在未知视图模型类型和一致的视图模型类型之间进行调解.可以使用UserControl .例如:

Yet another option, which like the above also addresses your concern about the displayed warning, is to create a "shim" (a.k.a. "adapter") object that mediates between the unknown view model type and a consistent one the UserControl can use. For example:

class ViewModelWrapper : NotifyPropertyChangedBase
{
    private readonly dynamic _viewModel;

    public ViewModelWrapper(object viewModel)
    {
        _viewModel = viewModel;
        HasSometimes = viewModel.GetType().GetProperty("Sometimes") != null;
        _viewModel.PropertyChanged += (PropertyChangedEventHandler)_OnPropertyChanged;
    }

    private void _OnPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        _RaisePropertyChanged(e.PropertyName);
    }

    public bool Always
    {
        get { return _viewModel.Always; }
        set { _viewModel.Always = value; }
    }

    public string OnOffAlways
    {
        get { return _viewModel.OnOffAlways; }
        set { _viewModel.OnOffAlways = value; }
    }

    public bool Sometimes
    {
        get { return HasSometimes ? _viewModel.Sometimes : false; }
        set { if (HasSometimes) _viewModel.Sometimes = value; }
    }

    public string OnOffSometimes
    {
        get { return HasSometimes ? _viewModel.OnOffSometimes : null; }
        set { if (HasSometimes) _viewModel.OnOffSometimes = value; }
    }

    private bool _hasSometimes;
    public bool HasSometimes
    {
        get { return _hasSometimes; }
        private set { _UpdateField(ref _hasSometimes, value); }
    }
}

此对象使用C#中的 dynamic 功能访问已知的属性值,并在构造上使用反射来确定是否应尝试访问 Sometimes (以及相关的 OnOffSometimes )属性(如果不存在,则通过 dynamic 类型的变量访问该属性会引发异常).

This object uses the dynamic feature in C# to access the known property values, and uses reflection on construction to determine whether or not it should try to access the Sometimes (and related OnOffSometimes) property (accessing the property via the dynamic-typed variable when it doesn't exist would throw an exception).

它还实现了 HasSometimes 属性,以便视图可以相应地动态调整自身.最后,它还代理基础 PropertyChanged 事件,以及委托的属性本身.

It also implements the HasSometimes property so that the view can dynamically adjust itself accordingly. Finally, it also proxies the underlying PropertyChanged event, to go along with the delegated properties themselves.

要使用此功能,需要 UserControl 后面的一些代码:

To use this, a little bit of code-behind for the UserControl is needed:

partial class UserControl1 : UserControl, INotifyPropertyChanged
{
    public ViewModelWrapper ViewModelWrapper { get; private set; }

    public UserControl1()
    {
        DataContextChanged += _OnDataContextChanged;
        InitializeComponent();
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void _OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        ViewModelWrapper = new ViewModelWrapper(DataContext);
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ViewModelWrapper)));
    }
}

有了这个,XAML就像您最初拥有的一样,但是对可选的 StackPanel 元素应用了一种样式,该样式具有根据属性是否存在来触发显示或隐藏该元素的触发器.是否:

With this, the XAML is mostly like what you originally had, but with a style applied to the optional StackPanel element that has a trigger to show or hide the element according to whether the property is present or not:

<UserControl x:Class="TestSO46736914MissingProperty.UserControl1"
             x:ClassModifier="internal"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:p="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:l="clr-namespace:TestSO46736914MissingProperty"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
  <Grid DataContext="{Binding ViewModelWrapper, RelativeSource={RelativeSource AncestorType=UserControl}}">
    <Grid.RowDefinitions>
      <RowDefinition Height="40"/>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="40"/>
      <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
    <StackPanel Grid.Row="1" Orientation ="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,5,0,5">
      <TextBlock Margin="5,0,5,0" Text="{Binding Path=OnOffAlways}"/>
      <CheckBox IsChecked="{Binding Path=Always}">
        <TextBlock Text="On/Off"/>
      </CheckBox>
    </StackPanel>
    <StackPanel Grid.Row="3" Orientation ="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,5,0,5">
      <StackPanel.Style>
        <p:Style TargetType="StackPanel">
          <p:Style.Triggers>
            <DataTrigger Binding="{Binding HasSometimes}" Value="False">
              <Setter Property="Visibility" Value="Collapsed"/>
            </DataTrigger>
          </p:Style.Triggers>
        </p:Style>
      </StackPanel.Style>
      <TextBlock Margin="5,0,5,0" Text="{Binding Path=OnOffSometimes}"/>
      <CheckBox IsChecked="{Binding Path=Sometimes}">
        <TextBlock Text="On/Off"/>
      </CheckBox>
    </StackPanel>
  </Grid>
</UserControl>

请注意,顶级 Grid 元素的 DataContext 设置为 UserControl ViewModelWrapper 属性,以便包含的元素使用该对象而不是父代码分配的视图模型.

Note that the top-level Grid element's DataContext is set to the UserControl's ViewModelWrapper property, so that the contained elements use that object instead of the view model assigned by the parent code.

(您可以忽略 p: XML名称空间…那只是因为Stack Overflow的XAML格式被使用默认XML的< Style/> 元素所混淆命名空间.)

(You can ignore the p: XML namespace…that's there only because Stack Overflow's XAML formatting gets confused by <Style/> elements that use the default XML namespace.)

虽然我通常更喜欢基于模板的方法,因为它是惯用的且本质上比较简单的方法,但是这种基于包装器的方法确实具有一些优点:

While I in general would prefer the template-based approach, as the idiomatic and inherently simpler one, this wrapper-based approach does have some advantages:

  • 它可以在以下情况下使用: UserControl 对象在与声明视图模型类型的程序集不同的程序集中声明了 UserControl 对象的情况下,并且后者不能被前者引用./li>
  • 它消除了基于模板的方法所需的冗余.IE.这种方法不必复制/粘贴模板的共享元素,而是对整个视图使用单个XAML结构,并根据需要显示或隐藏该视图的元素.
  • It can be used in situations where the UserControl object is declared in an assembly different from the one where the view model types are declared, and where the latter assembly cannot be referenced by the former.
  • It removes the redundancy that is required by the template-based approach. I.e. rather than having to copy/paste the shared elements of the templates, this approach uses a single XAML structure for the entire view, and shows or hides elements of that view as appropriate.

为完整起见,这是上面的 ViewModelWrapper 类使用的 NotifyPropertyChangedBase 类:

For completeness, here is the NotifyPropertyChangedBase class used by the ViewModelWrapper class above:

class NotifyPropertyChangedBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void _UpdateField<T>(ref T field, T newValue,
        Action<T> onChangedCallback = null,
        [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, newValue))
        {
            return;
        }

        T oldValue = field;

        field = newValue;
        onChangedCallback?.Invoke(oldValue);
        _RaisePropertyChanged(propertyName);
    }

    protected void _RaisePropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

对于它的价值,我更喜欢这种方法在每个模型对象中重新实现 INotifyPropertyChanged 接口.该代码更简单,更易于编写,更易于阅读,并且不易出错.

For what it's worth, I prefer this approach to re-implementing the INotifyPropertyChanged interface in each model object. The code is a lot simpler and easier to write, simpler to read, and less prone to errors.

这篇关于仅在存在的情况下绑定到属性的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆