2015-10-15 45 views
0

注意:您現在可以在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(); 
    } 
} 
} 

應用。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(); 
    } 
} 
} 

這裏的目的是要對MainWindow變化在TextBox價值所選項目更改時所選項目的ID。我可以通過在我的LocationListView上創建一個SelectedItemChanged事件,然後在處理程序中手動設置屬性來做到這一點,但這似乎是一種破解。如果你放置一個<ListView ItemsSource="{Binding SillyStuff}" SelectedItem="{Binding MySelectedLocation}" DockPanel.Dock="Top"/>而不是我的列表控件,這就像一個魅力,所以我應該能夠使我的控制工作也這樣。

編輯:更改MainViewModel執行INotifyPropertyChanged根據彼得的指示。

+3

視圖模型應該實現'INotifyPropertyChanged',它們不需要是'DependencyObject's並且不需要包含'DependencyProperty's。 UI控件已經具備了這些和那些足以讓綁定系統使用的功能。視圖模型只需在其屬性更改時通知UI。 –

+0

好吧,看起來我正在使用這種做法,因爲我查看了展示MVVM的第一個教程(以及其他模式)。我已經改變了這一點,但這似乎不是問題。 –

+0

@Bart示例現在在GitHub上。 https://github.com/ReasonSharp/MyTestRepo –

回答

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設置爲空,然後將其設置爲新選擇的項目。此外,再生項目,selectedLV(什麼是「LV」)時被設置爲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> 

這可能需要一些更多的修改,以使它看起來不錯,但是這是要點它。或者,您可以創建一個附加的行爲來照顧您所需的選擇行爲。

+0

'SetCurrentValue'不起作用,即行爲與以前相同。至於取消註冊,我把這一點放在一邊,因爲解決方案不是微不足道的。我需要一個私有變量來存儲舊集合,因爲在調用'LocationsChanged'時,我已經失去了對它的引用。現在你提到了,我注意到我也沒有註銷'B_Click'。我在想,如果我不能用'SelectedLocation'解決問題,那麼沒有必要修復泄漏,因爲我根本不會使用控件。 –

+0

哦,我忘記了'DependencyPropertyChangedEventArgs'中有'NewValue'和'OldValue'。我的錯。那麼微不足道。但是,這並沒有幫助我的主要問題。 –

+0

我有一段時間來測試你的代碼,發現了其餘的問題。我還發現了一些小問題。至於取消註冊一個事件監聽器,只需使用一個私有變量即可。將這些鬆散的結果聯繫起來,然後將它們變成需要數小時才能追趕的bug。 ;) –

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現在可以使用簡單的Path綁定綁定到它的DataContext

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

我希望這會有所幫助。

+0

我通過使用'ElementName = usr'將我的'TextBox.Text'屬性綁定到控件的'SelectedLocations.ID'屬性來實現它。這仍然不會對我的'MainViewModel'中的屬性做任何事情,並且我需要在工作條件下對其進行更改。就像我在我的帖子中所說的,我根本沒有使用'this.DataContext = this;',所以根據你的說法,一切都應該順利進行,但事實並非如此。另外,爲什麼我的主窗口需要將數據上下文設置爲控件?如果我放置一個'ListView'而不是我的控件,我不需要設置它的'DataContext',但這個例子完美地工作。 –