2016-03-26 18 views
0

Ich habe ein wirklich seltsames Problem, von dem ich fast überzeugt bin, dass es ein Bug ist.Bindung an UserControl in einer UserControl-Instanz Synchronisierung mit einer anderen Instanz

Ich habe drei UserControls, FolderView, LocalFolderView, RemoteFolderView. LocalFolderView und RemoteFolderView erben beide FolderView und werden in zwei anderen Steuerelementen LocalExplorer und RemoteExplorer verwandt.

LocalExplorer/RemoteExplorer haben eine Liste von Strings, die ich an die FolderView binde.

Das Problem ist, wenn ich mehr als 1 Instanz von LocalExplorer/RemoteExplorer, die ListBox in der FolderView für beide Explorer die gleichen Elemente anzeigen, aber die Abhängigkeitseigenschaften für die Steuerelemente sind scheinbar unterschiedlich.

Der Code ist wirklich lang, also werde ich versuchen, so viel wie möglich zu kondensieren. Momentan glaube ich, dass das Problem darin liegt, wie ich Dinge verbinde.

Hier ist die Kontrolle I mehr als eine Instanz dieses haben den Fehler aufweist:

LocalExplorer.xaml (RemoteExplorer.xaml folgt identisches Muster):

<UserControl x:Class="LocalExplorer" 
      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:MyNamespace" 
      mc:Ignorable="d" 
      Width="Auto" 
      Height="Auto" 
      ClipToBounds="True" 
      x:Name="explorer"> 
    <local:ExplorerBase Path="{Binding ElementName=explorer, Path=Path}" Orientation="{Binding ElementName=explorer, Path=Orientation}"> 
     <local:ExplorerBase.FolderView> 
      <local:LocalFolderView x:Name="FolderView" Path="{Binding Path, RelativeSource={RelativeSource AncestorType={x:Type local:LocalExplorer}}}"/> 
     </local:ExplorerBase.FolderView> 
    </local:ExplorerBase> 
</UserControl> 

LocalExplorer.xaml.cs (RemoteExplorer. xaml.cs folgt identisches Muster):

public partial class Explorer : UserControl 
{ 
    #region Explorer 

    public Explorer() 
    { 
     InitializeComponent(); 
    } 

    #endregion 

    #region Dependency Properties 

    public static DependencyProperty PathProperty = DependencyProperty.Register("Path", typeof(string), typeof(Explorer), new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); 
    public string Path 
    { 
     get 
     { 
      return (string)GetValue(PathProperty); 
     } 
     set 
     { 
      SetValue(PathProperty, value); 
     } 
    } 

    #endregion 
} 

Weiter ExplorerBase ist, die UI-Logik spezifisch für alle Explorers beherbergt:

ExplorerBase.cs:

public partial class ExplorerBase : Control 
{ 
    public ExplorerBase() 
    { 
     this.DefaultStyleKey = typeof(ExplorerBase); 
    } 

    public override void OnApplyTemplate() 
    { 
     base.ApplyTemplate(); 
    } 

    public static readonly DependencyProperty FolderViewProperty = DependencyProperty.Register("FolderView", typeof(object), typeof(ExplorerBase), null); 
    public object FolderView 
    { 
     get 
     { 
      return GetValue(FolderViewProperty); 
     } 
     set 
     { 
      SetValue(FolderViewProperty, value); 
     } 
    } 

    public static DependencyProperty PathProperty = DependencyProperty.Register("Path", typeof(string), typeof(ExplorerBase), new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); 
    public string Path 
    { 
     get 
     { 
      return (string)GetValue(PathProperty); 
     } 
     set 
     { 
      SetValue(PathProperty, value); 
     } 
    } 
} 

I Schablone es Themen/generic.xaml Ansatz:

<Style TargetType="{x:Type Imagin.Controls:ExplorerBase}"> 
    <Setter Property="Template"> 
     <Setter.Value> 
      <ControlTemplate TargetType="Imagin.Controls:ExplorerBase"> 
       <ContentPresenter Content="{TemplateBinding FolderView}"/> 
      </ControlTemplate> 
     </Setter.Value> 
    </Setter> 
</Style> 

Und schließlich die Folderview, die meiner Meinung nach ist das, was den Fehler hat. Die FolderView ist eine Basis für die tatsächlich verwendeten Steuerelemente LocalFolderView und RemoteFolderView. Beachten Sie, dass der Fehler unabhängig davon auftritt, ob ich LocalExplorer und RemoteExplorer oder eine von beiden verwende. Ich habe insgesamt nur zwei Instanzen gleichzeitig getestet.

FolderView.xaml:

<UserControl x:Class="FolderView" 
      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" 
      mc:Ignorable="d" 
      Height="Auto" 
      Width="Auto" 
      x:Name="folderView"> 
    <UserControl.Resources> 
     <BooleanToVisibilityConverter x:Key="BoolToVisibility" /> 
     <Imagin.Data:InverseBooleanToVisibilityConverter x:Key="InverseBoolToVisibility" /> 
     <Grid> 
      <ListBox x:Name="ListBox" AllowDrop="True" ItemsSource="{Binding Path=Items, RelativeSource={RelativeSource AncestorType={x:Type local:FolderView}}}" SelectionMode="Extended" ScrollViewer.HorizontalScrollBarVisibility="Disabled" ScrollViewer.VerticalScrollBarVisibility="Auto" IsSynchronizedWithCurrentItem="True"> 
       <ListBox.ItemsPanel> 
        <ItemsPanelTemplate> 
         <WrapPanel IsItemsHost="True"/> 
        </ItemsPanelTemplate> 
       </ListBox.ItemsPanel> 
       <ListBox.Resources> 
        <DataTemplate DataType="{x:Type local:BindableFile}"> 
         <local:Thumbnail FilePath="{Binding Path}" IsCheckBoxEnabled="False" ToolTip="{Binding ToolTip}" Title="{Binding Name}" Width="Auto" Height="Auto"/> 
        </DataTemplate> 
        <DataTemplate DataType="{x:Type local:BindableFolder}"> 
         <local:Thumbnail FilePath="{Binding Path}" IsCheckBoxEnabled="False" ToolTip="{Binding ToolTip}" Title="{Binding Name}" Width="Auto" Height="Auto"/> 
        </DataTemplate> 
       </ListBox.Resources> 
       <ListBox.ItemContainerStyle> 
        <Style TargetType="{x:Type ListBoxItem}"> 
         <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> 
        </Style> 
       </ListBox.ItemContainerStyle> 
      </ListBox> 
      <DataGrid x:Name="DataGrid" ItemsSource="{Binding Items, RelativeSource={RelativeSource AncestorType={x:Type local:FolderView}}}" AutoGenerateColumns="False" BorderThickness="0" AlternationCount="2" GridLinesVisibility="None" HeadersVisibility="Column" CanUserAddRows="False" CanUserResizeColumns="True" IsSynchronizedWithCurrentItem="True" AllowDrop="True"> 
      </DataGrid> 
     </Grid> 
</UserControl> 

FolderView.xaml.cs:

public abstract partial class FolderView : UserControl 
{ 
    #region DependencyProperties 

    public static DependencyProperty ItemsProperty = DependencyProperty.Register("Items", typeof(ObservableCollection<BindablePath>), typeof(FolderView), new FrameworkPropertyMetadata(new ObservableCollection<BindablePath>(), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); 
    public ObservableCollection<BindablePath> Items 
    { 
     get 
     { 
      return (ObservableCollection<BindablePath>)GetValue(ItemsProperty); 
     } 
     set 
     { 
      SetValue(ItemsProperty, value); 
     } 
    } 

    public static DependencyProperty PathProperty = DependencyProperty.Register("Path", typeof(string), typeof(FolderView), new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnPathChanged)); 
    public string Path 
    { 
     get 
     { 
      return (string)GetValue(PathProperty); 
     } 
     set 
     { 
      SetValue(PathProperty, value); 
     } 
    } 
    private static void OnPathChanged(DependencyObject Object, DependencyPropertyChangedEventArgs e) 
    { 
     FolderView FolderView = (FolderView)Object; 
     FolderView.Refresh(); 
     FolderView.SearchTextBox.Text = string.Empty; 
    } 

    #endregion 

    #region Methods 

    public virtual void GetItems(string Path, out List<string> Folders, out List<string> Files) 
    { 
     Folders = default(List<string>); 
     Files = default(List<string>); 
    } 

    /// <summary> 
    /// Refreshes current path with contents. 
    /// </summary> 
    public virtual void Refresh() 
    { 
     //Used to debug property values at runtime. So far the values for each object instance are unique. 
     foreach (PropertyDescriptor descriptor in TypeDescriptor.GetProperties(this)) 
     { 
      string name = descriptor.Name; 
      object value = descriptor.GetValue(this); 
      Console.WriteLine("{0}={1}", name, value); 
     } 
    } 

    /// <summary> 
    /// Populates controls with actual items via binding. We must do this on UI thread. This occurs immediately after <Refresh()>. 
    /// </summary> 
    /// <param name="Folders"></param> 
    /// <param name="Files"></param> 
    public virtual void Populate(List<FtpListItem> Folders, List<FtpListItem> Files) 
    { 
    } 

    public virtual void Populate(List<string> Folders, List<string> Files) 
    { 
    } 

    #endregion 

    #region FolderView 

    public FolderView() 
    { 
     InitializeComponent(); 
    } 

    #endregion 
} 

LocalFolderView.cs (RemoteFolderView.cs folgt identisches Muster):

public sealed class LocalFolderView : FolderView 
{ 
    public override void GetItems(string Path, out List<string> Folders, out List<string> Files) 
    { 
     //These are my own functions 
     Folders = Directory.GetDirectories(Path); 
     Files = Directory.GetFiles(Path, null); 
    } 

    public override void Populate(List<string> Folders, List<string> Files) 
    { 
     int NumFolders = Folders.Count, NumFiles = Files.Count; 
     this.IsEmpty = NumFolders == 0 && NumFiles == 0 ? true : false; 
     if (Folders == null || Files == null || (NumFolders == 0 && NumFiles == 0)) return; 
     for (int j = 0, Count = NumFolders; j < Count; j++) 
     { 
      this.Items.Add(new BindableFolder(Folders[j])); 
     } 
     for (int j = 0, Count = NumFiles; j < Count; j++) 
     { 
      this.Items.Add(new BindableFile(Files[j])); 
     } 
    } 

    public override void Refresh() 
    { 
     base.Refresh(); 
     this.Items.Clear(); 
     //If directory doesn't exist, we don't want to enter it. 
     if (!System.IO.Directory.Exists(this.Path)) return; 
     List<string> Folders = null; 
     List<string> Files = null; 
     string CurrentPath = this.Path; 

     BackgroundWorker Worker = new BackgroundWorker(); 
     Worker.DoWork += (s, e) => 
     { 
      this.GetItems(CurrentPath, out Folders, out Files); 
     }; 
     Worker.RunWorkerCompleted += (s, e) => 
     { 
      //Start populating items 
      var DispatcherOperation = Application.Current.Dispatcher.BeginInvoke(new Action(() => this.Populate(Folders, Files))); 
     }; 
     Worker.RunWorkerAsync(); 
    } 
} 

Dinge zu beachten :

  1. Die DataGrids in den FolderViews in beiden Fällen werden ebenfalls mit identischen Elementen gefüllt.
  2. Die Pfadeigenschaft für jede FolderView-Instanz ist unterschiedlich.
  3. Dies tritt nur auf, nachdem die zweite Instanz mit Elementen gefüllt und dann versucht wird, die erste Instanz zu füllen. Wenn ich zuerst die erste Instanz bevölkere, passiert der zweiten überhaupt nichts.
  4. Wenn ich sage, dass beide Instanzen mit identischen Elementen gefüllt werden, meine ich, dass, wenn ich die erste auflege, die ersten Elemente in der zweiten erscheinen. Und wenn ich die Sekunde bevölkere, erscheinen die Elemente der Sekunde in der ersten.
  5. Und auch, wenn ich "populate" sage, ich meine nur, ich setze die Path Eigenschaft auf die FolderView.

Dinge, die ich habe versucht:

  1. die Art und Weise ändern I binden. Anstatt beispielsweise wie Binding ElementName=explorer, Path=Property zu binden, würde ich es zu Binding Property, RelativeSource={RelativeSource AncestorType={x:Type local:UserControlType}} ändern.
  2. Entfernen x:Name Eigenschaften aus verschiedenen Elementen.
  3. Dumping FolderView-Eigenschaften/Werte. Ein Beispiel dafür oben in Quelle.

Ehrlich gesagt, weiß ich nicht, wie sonst zu debuggen. Ist das ein Fehler oder ist meine Bindungslogik nicht so logisch?

bearbeiten

Hier ist, wie ich zwei Explorer-Instanzen angezeigt:

<local:LocalExplorer /> 
<local:RemoteExplorer/> 

Da sie beide ihre eigenen Instanzen, ich sehe nicht, wie entweder versehentlich auf einen anderen binden könnte, besonders wenn man bedenkt, wie tief verschachtelt sind die ListBoxen in der visuellen Struktur.

Antwort

1

Das Problem liegt in der Abhängigkeitseigenschaftsregistrierung der Items-Eigenschaft.

public static DependencyProperty ItemsProperty = DependencyProperty.Register("Items", 
typeof(ObservableCollection<BindablePath>), typeof(FolderView), 
new FrameworkPropertyMetadata(new ObservableCollection<BindablePath>(), 
        FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); 

Wie Sie Registrierung ist statisch sehen kann und wird auf Typ registriert und nicht auf Instanz. Da der vorgegebene Wert new ObservableCollection<BindablePath>() ist, wird dieselbe Instanz für alle Instanzen von FolderView freigegeben. Aus diesem Grund wird jedes Mal, wenn ein neues Element hinzugefügt wird, dieses in allen Instanzen angezeigt, da sich die Items-Eigenschaft auf eine Instanz bezieht.

Als Daumenregel sollten Sie bei der Registrierung von Abhängigkeitseigenschaften immer vermeiden, neue Instanzen für Referenztypen bereitzustellen.


Lösung:

Make Standardwert als null und stattdessen initialisieren es neue Instanz von Konstruktor von Folderview (pro Instanz).

new FrameworkPropertyMetadata(null,FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); 
+1

Omg, DANKE. –

Verwandte Themen