注意:您现在可以在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
。
答案 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>
这可能需要进行一些修改以使其看起来不错,但这是它的要点。或者,您可以创建一个附加行为来处理您期望的选择行为。