我绑定到依赖项属性的方式有什么问题?

时间:2015-10-15 07:36:24

标签: c# wpf xaml mvvm data-binding

注意:您现在可以在github上找到下面的项目。 https://github.com/ReasonSharp/MyTestRepo

我正在使用滚动条创建一个简单的列表控件,该滚动条将显示我传递给它的对象集合。当用户点击一个项目时,我希望它成为一个选定的项目,当他再次点击它时,我希望它不被选中。我将所选项目存储在SelectedLocation属性中。在调试时,属性设置适当。但是,如果我将此列表控件(LocationListView)放到窗口并绑定到控件中的SelectedLocation(如SelectedLocation="{Binding MyLocation}"),则绑定将不起作用,如果我尝试在同一个窗口中的另一个绑定中使用此MyLocation(即<TextBox Text="{Binding MyLocation.ID}"/>,其中ID是依赖属性),当我在列表中选择不同的项时,绑定不会显示任何更改

最小的例子有点大,请耐心等待:

列表控制

XAML

<UserControl x:Class="MyListView.LocationListView"
             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:MyListView"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
 <Grid x:Name="locationListView">
  <ScrollViewer HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Auto">
   <StackPanel x:Name="myStackPanel"/>
  </ScrollViewer>
 </Grid>
</UserControl>

背后的代码

using System.Collections;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;

namespace MyListView {
 public partial class LocationListView : UserControl {
  #region Dependency Properties
  public IEnumerable Locations {
   get { return (IEnumerable)GetValue(LocationsProperty); }
   set { SetValue(LocationsProperty, value); }
  }
  public static readonly DependencyProperty LocationsProperty =
  DependencyProperty.Register("Locations", typeof(IEnumerable), typeof(LocationListView), new PropertyMetadata(null, LocationsChanged));

  public MyObject SelectedLocation {
   get { return (MyObject)GetValue(SelectedLocationProperty); }
   set { SetValue(SelectedLocationProperty, value); }
  }
  public static readonly DependencyProperty SelectedLocationProperty =
  DependencyProperty.Register("SelectedLocation", typeof(MyObject), typeof(LocationListView), new PropertyMetadata(null));
  #endregion

  private static void LocationsChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) {
   ((LocationListView)o).RegenerateLocations();
   if (((LocationListView)o).Locations is ObservableCollection<MyObject>) {
    var l = ((LocationListView)o).Locations as ObservableCollection<MyObject>;
    l.CollectionChanged += ((LocationListView)o).L_CollectionChanged;
   }
  }

  private void L_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) {
   RegenerateLocations();
  }

  private Button selectedLV = null;

  public LocationListView() {
   InitializeComponent();
  }

  private void RegenerateLocations() {
   if (Locations != null) {
    myStackPanel.Children.Clear();
    foreach (var l in Locations) {
     var b = new Button();
     b.Content = l;
     b.Click += B_Click;
     myStackPanel.Children.Add(b);
    }
   }
   selectedLV = null;
  }

  private void B_Click(object sender, RoutedEventArgs e) {
   var lv = (sender as Button)?.Content as MyObject;
   if (selectedLV != null) {
    lv.IsSelected = false;
    if ((selectedLV.Content as MyObject) == SelectedLocation) {
     SelectedLocation = null;
     selectedLV = null;
    }
   }
   if (lv != null) {
    SelectedLocation = lv;
    selectedLV = sender as Button;
    lv.IsSelected = true;
   }
  }
 }
}

请注意缺少this.DataContext = this;行。如果我使用它,我会得到以下绑定表达式路径错误:

System.Windows.Data Error: 40 : BindingExpression path error: 'SillyStuff' property not found on 'object' ''LocationListView' (Name='')'. BindingExpression:Path=SillyStuff; DataItem='LocationListView' (Name=''); target element is 'LocationListView' (Name=''); target property is 'Locations' (type 'IEnumerable')
System.Windows.Data Error: 40 : BindingExpression path error: 'MySelectedLocation' property not found on 'object' ''LocationListView' (Name='')'. BindingExpression:Path=MySelectedLocation; DataItem='LocationListView' (Name=''); target element is 'LocationListView' (Name=''); target property is 'SelectedLocation' (type 'MyObject')

使用(this.Content as FrameworkElement).DataContext = this;不会产生这些错误,但它也不起作用。

主窗口

XAML

<Window x:Class="MyListView.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:MyListView"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
 <Grid>
  <DockPanel LastChildFill="True" HorizontalAlignment="Stretch" VerticalAlignment="Top">
   <local:LocationListView Locations="{Binding SillyStuff}" SelectedLocation="{Binding MySelectedLocation}" DockPanel.Dock="Top"/>
   <TextBox Text="{Binding MySelectedLocation.ID}" DockPanel.Dock="Top"/>
  </DockPanel>
 </Grid>
</Window>

背后的代码

using System.Windows;
using Microsoft.Practices.Unity;

namespace MyListView {
 public partial class MainWindow : Window {
  private MainViewModel vm;

  public MainWindow() {
   InitializeComponent();
  }

  [Dependency] // Unity
  internal MainViewModel VM {
   set {
    this.vm = value;
    this.DataContext = vm;
   }
  }
 }
}

MainViewModel

using System.Collections.ObjectModel;
using System.ComponentModel;

namespace MyListView {
 class MainViewModel : INotifyPropertyChanged {
  public event PropertyChangedEventHandler PropertyChanged;
  protected virtual void OnPropertyChanged(object sender, PropertyChangedEventArgs e) {
   if (PropertyChanged != null)
    PropertyChanged(sender, e);
  }

  private MyObject mySelectedLocation;
  public MyObject MySelectedLocation {
   get { return mySelectedLocation; }
   set {
    mySelectedLocation = value;
    OnPropertyChanged(this, new PropertyChangedEventArgs("MySelectedLocation"));
   }
  }

  public ObservableCollection<MyObject> SillyStuff {
   get; set;
  }

  public MainViewModel() {
   var cvm1 = new MyObject();
   cvm1.ID = 12345;

   var cvm2 = new MyObject();
   cvm2.ID = 54321;

   var cvm3 = new MyObject();
   cvm3.ID = 15243;

   SillyStuff = new ObservableCollection<MyObject>();
   SillyStuff.Add(cvm1);
   SillyStuff.Add(cvm2);
   SillyStuff.Add(cvm3);
  }
 }
}

为MyObject

using System.Windows;

namespace MyListView {
 public class MyObject : DependencyObject {
  public int ID {
   get { return (int)GetValue(IDProperty); }
   set { SetValue(IDProperty, value); }
  }
  public static readonly DependencyProperty IDProperty =
      DependencyProperty.Register("ID", typeof(int), typeof(MyObject), new PropertyMetadata(0));

  public bool IsSelected {
   get; set;
  }

  public override string ToString() {
   return ID.ToString();
  }
 }
}

App.xaml - 只是为了保存打字的人

XAML

<Application x:Class="MyListView.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:MyListView">
    <Application.Resources>

    </Application.Resources>
</Application>

背后的代码

using System.Windows;
using Microsoft.Practices.Unity;

namespace MyListView {
 public partial class App : Application {
  protected override void OnStartup(StartupEventArgs e) {
   base.OnStartup(e);

   UnityContainer container = new UnityContainer();
   var mainView = container.Resolve<MainWindow>();
   container.Dispose();
   mainView.Show();
  }
 }
}

此处的目标是,只要所选项目发生更改,TextBox MainWindow上的值就会更改为所选项目的ID。我可以通过在SelectedItemChanged上创建LocationListView事件,然后在处理程序中手动设置属性来实现,但这似乎是一个黑客攻击。如果你放置一个<ListView ItemsSource="{Binding SillyStuff}" SelectedItem="{Binding MySelectedLocation}" DockPanel.Dock="Top"/>而不是我的列表控件,这就像一个魅力,所以我应该能够让我的控件以这种方式工作。

修改:根据彼得的指示更改MainViewModel以实施INotifyPropertyChanged

2 个答案:

答案 0 :(得分:1)

哦,小伙,那是很多代码。

让我首先强调一个常见的错误,即将控件DataContext设置为自身。这应该避免,因为它往往会搞砸一切。

因此。避免这样做:

this.DataContext = this;

UserControl本身不负责设置自己的DataContext,它应由父控件负责(例如设置Window就像这样:

<Window ...>
    <local:MyUserControl DataContext="{Binding SomeProperty}" ... />

如果您的UserControl设置了自己的DataContext,那么它将覆盖Window设置其DataContext的内容。这将导致绝对一切的搞砸。

要绑定到UserControl依赖关系属性,只需向控件提供x:Name并使用ElementName绑定,如下所示:

<UserControl ...
    x:Name="usr">
    <TextBlock Text="{Binding SomeDependencyProperty, ElementName=usr}" ... />

这里需要注意的重要一点是,DataContext并未设置 ,因此您的父Window可以自由设置控制DataContext以及它需要的任何东西。

此外,您的UserControl现在可以使用简单的DataContext绑定绑定到Path

<UserControl ...
    x:Name="usr">
    <TextBlock Text="{Binding SomeDataContextProperty}" ... />

我希望这会有所帮助。

答案 1 :(得分:1)

主要问题

当您在自定义控件中选择一个项目时,B_Click会将其分配给SelectedLocation属性,该属性在内部调用SetValue。但是,这会覆盖SelectedLocation上的绑定 - 换句话说,在调用SelectedLocation之后不再绑定任何东西。请改用SetCurrentValue来保留绑定。

但是,绑定默认情况下不会更新其源。您必须将Mode设置为TwoWay。您可以在XAML中执行此操作:SelectedLocation="{Binding MySelectedLocation, Mode=TwoWay}",或者将依赖项属性标记为默认使用TwoWay绑定:new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, LocationsChanged)

最后,确保您的绑定路径正确。您的文本框绑定到SelectedLocation,而该属性的名称为MySelectedLocation。这些问题通常记录在调试输出中,在这种情况下,您应该收到如下消息:

System.Windows.Data Error: 40 : BindingExpression path error: 'SelectedLocation' property not found on 'object' ''MainViewModel' (HashCode=8757408)'. BindingExpression:Path=SelectedLocation.ID; DataItem='MainViewModel' (HashCode=8757408); target element is 'TextBox' (Name=''); target property is 'Text' (type 'String')

其他问题

我还发现了一些其他问题:当您设置了另一个集合时,您并未取消注册L_CollectionChanged,如果删除了该集合,则您不会清除可见的内容项目。 B_Click中的代码也很麻烦:在确定它不为空之前,您还要访问lv,如果用户点击未选择的按钮,则您需要设置SelectedLocation在将其设置为新选择的项目之前为null。此外,在重新生成项目时,selectedLV(什么&#39; lv&#39;?)设置为null,但SelectedLocation保持不变...

还有一点提示:您的OnPropertyChanged方法只需要一个参数:string propertyName。使其成为可选项并使用[CallerMemberName]属性进行标记,因此属性设置器需要做的就是不带参数调用它。编译器将为您插入调用属性名称。

<强>替代

就个人而言,我只使用ListView与自定义ItemTemplate

<ListView ItemsSource="{Binding MyLocations}" SelectedItem="{Binding MySelectedLocation}" SelectionMode="Single">
    <ListView.ItemTemplate>
        <DataTemplate>
            <ToggleButton IsChecked="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=ListViewItem}}" Content="{Binding}" />
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

这可能需要进行一些修改以使其看起来不错,但这是它的要点。或者,您可以创建一个附加行为来处理您期望的选择行为。