2010-05-05 1 views
3

Я создаю приложения WPF с помощью MVVM рамки, и я принял несколько функций из статьи Джоша Смита на MVVM here...Связывание TabControl ItemsSource к ObservableCollection из ViewModels вызывает содержание, чтобы освежить в фокусе

Самое главное, Я привязываю TabControl к ObservableCollection из ViewModels. Это означает, что я использую интерфейс MDI с вкладками, который отображает UserControl как содержимое TabItem. Проблема, которую я вижу в своем приложении, заключается в том, что когда у меня есть несколько вкладок, и я переворачиваюсь между вкладками, содержимое ссылается на каждый раз, когда я меняю вкладки.

Если вы загрузите исходный код Джоша Смита, вы увидите, что его приложение имеет ту же проблему. Например, нажмите кнопку «Просмотреть всех клиентов» и прокрутите вниз до списка ListView. Затем нажмите кнопку «Создать нового клиента». Когда вы вернетесь к просмотру «Все клиенты», вы заметите, что ListView прокручивается назад. Если вы вернетесь на вкладку «Новый клиент» и поместите курсор в один из текстовых полей, перейдите на вкладку «Все клиенты» и обратно, вы заметите, что курсор теперь исчез.

Я предполагаю, что это потому, что я использую ObservableCollection, но я не могу быть уверен. Есть ли способ предотвратить обновление содержимого вкладки при получении фокуса?

EDIT: Я нашел свою проблему, когда запустил профайлер в своем приложении. Я определяю DataTemplate для моего ViewModels поэтому он знает, как оказать ViewModel, когда он отображается на вкладке ... как так:

<DataTemplate DataType="{x:Type vm:CustomerViewModel}"> 
    <vw:CustomerView/> 
</DataTemplate> 

Поэтому, когда я перейти на другую вкладку, он должен повторно -создайте ViewModel снова. Я исправил это временно, изменив свой ObservableCollection ViewModels на ObservableCollection of UserControls. Тем не менее, я по-прежнему хотел бы использовать DataTemplates, если это возможно. Есть ли способ сделать работу DataTemplate?

ответ

0

Я считаю, что это имеет какое-то отношение к режиму привязки - вы попробовали {Binding Mode = OneWay}? Кроме того, привязка к любому объекту DataSource вызовет ту же проблему.

Я не уверен, что привязка в будущем будет желательным поведением, которое вы ищете, но я бы начал там.

+0

Я пробовал, но это не сработало. Я обновил свой вопрос ... дайте мне знать, можете ли вы внести какие-либо данные. Благодарю. – Brent

0

Это не имеет никакого отношения к ObservableCollection. Это связано с тем, что представление для клиента повторно используется, а WPF создает новый вид для каждого клиента.

Возможно, вы можете ознакомиться с образцом приложения Writer от WPF Application Framework (WAF). Он также реализует TabControl для интерфейса MDI с вкладками, но не страдает от проблемы, упомянутой в вашем сообщении. Проблема решена в Writer, создав собственный UserControl для каждой вкладки документа.

+0

Спасибо @jbe, я знаю, что я мог бы решить проблему с помощью ObservableCollection UserControls, но я предпочел бы использовать ObservableCollection of ViewModels, если это возможно. Я обновил свой вопрос ... дайте мне знать, можете ли вы внести какие-либо данные. Благодарю. – Brent

+1

Когда я начал с шаблона MVVM, я использовал механизм DataTemplate WPF, а также для соединения View вместе с ViewModel. Но я столкнулся с несколькими проблемами, такими как тот, который вы упомянули здесь. Вот почему я связываю их с MEF вместе. WPF просто получает Views (UserControls), и поэтому мне больше не нужен DataTemplate для этого сценария. – jbe

1

Поведение WPF по умолчанию заключается в выгрузке элементов, которые не видны, что включает в себя выгрузку TabItems, которые не видны. Это означает, что когда вы вернетесь на вкладку, TabItem снова загружается, и все, что не связано (например, положение прокрутки), будет сброшено.

Был хороший сайт here, который содержит код для расширения TabControl и не позволяет ему уничтожать его TabItems при переключении вкладок, однако теперь он больше не существует.

Вот код, который я использую для предотвращения этой проблемы. Сначала он был с этого сайта, хотя я внес некоторые изменения в него.Он сохраняет ContentPresenter TabItems при переключении вкладок и использует его для перерисовки TabItem, когда вы возвращаетесь на страницу. Это занимает немного больше памяти, однако я считаю, что это лучше по производительности, так как TabItem больше не нужно воссоздавать все элементы управления, которые были на нем.

// Extended TabControl which saves the displayed item so you don't get the performance hit of 
// unloading and reloading the VisualTree when switching tabs 

// Obtained from http://eric.burke.name/dotnetmania/2009/04/26/22.09.28 
// and made a some modifications so it reuses a TabItem's ContentPresenter when doing drag/drop operations 

[TemplatePart(Name = "PART_ItemsHolder", Type = typeof(Panel))] 
public class TabControlEx : System.Windows.Controls.TabControl 
{ 
    // Holds all items, but only marks the current tab's item as visible 
    private Panel _itemsHolder = null; 

    // Temporaily holds deleted item in case this was a drag/drop operation 
    private object _deletedObject = null; 

    public TabControlEx() 
     : base() 
    { 
     // this is necessary so that we get the initial databound selected item 
     this.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged; 
    } 

    /// <summary> 
    /// if containers are done, generate the selected item 
    /// </summary> 
    /// <param name="sender"></param> 
    /// <param name="e"></param> 
    void ItemContainerGenerator_StatusChanged(object sender, EventArgs e) 
    { 
     if (this.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated) 
     { 
      this.ItemContainerGenerator.StatusChanged -= ItemContainerGenerator_StatusChanged; 
      UpdateSelectedItem(); 
     } 
    } 

    /// <summary> 
    /// get the ItemsHolder and generate any children 
    /// </summary> 
    public override void OnApplyTemplate() 
    { 
     base.OnApplyTemplate(); 
     _itemsHolder = GetTemplateChild("PART_ItemsHolder") as Panel; 
     UpdateSelectedItem(); 
    } 

    /// <summary> 
    /// when the items change we remove any generated panel children and add any new ones as necessary 
    /// </summary> 
    /// <param name="e"></param> 
    protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e) 
    { 
     base.OnItemsChanged(e); 

     if (_itemsHolder == null) 
     { 
      return; 
     } 

     switch (e.Action) 
     { 
      case NotifyCollectionChangedAction.Reset: 
       _itemsHolder.Children.Clear(); 

       if (base.Items.Count > 0) 
       { 
        base.SelectedItem = base.Items[0]; 
        UpdateSelectedItem(); 
       } 

       break; 

      case NotifyCollectionChangedAction.Add: 
      case NotifyCollectionChangedAction.Remove: 

       // Search for recently deleted items caused by a Drag/Drop operation 
       if (e.NewItems != null && _deletedObject != null) 
       { 
        foreach (var item in e.NewItems) 
        { 
         if (_deletedObject == item) 
         { 
          // If the new item is the same as the recently deleted one (i.e. a drag/drop event) 
          // then cancel the deletion and reuse the ContentPresenter so it doesn't have to be 
          // redrawn. We do need to link the presenter to the new item though (using the Tag) 
          ContentPresenter cp = FindChildContentPresenter(_deletedObject); 
          if (cp != null) 
          { 
           int index = _itemsHolder.Children.IndexOf(cp); 

           (_itemsHolder.Children[index] as ContentPresenter).Tag = 
            (item is TabItem) ? item : (this.ItemContainerGenerator.ContainerFromItem(item)); 
          } 
          _deletedObject = null; 
         } 
        } 
       } 

       if (e.OldItems != null) 
       { 
        foreach (var item in e.OldItems) 
        { 

         _deletedObject = item; 

         // We want to run this at a slightly later priority in case this 
         // is a drag/drop operation so that we can reuse the template 
         this.Dispatcher.BeginInvoke(DispatcherPriority.DataBind, 
          new Action(delegate() 
         { 
          if (_deletedObject != null) 
          { 
           ContentPresenter cp = FindChildContentPresenter(_deletedObject); 
           if (cp != null) 
           { 
            this._itemsHolder.Children.Remove(cp); 
           } 
          } 
         } 
         )); 
        } 
       } 

       UpdateSelectedItem(); 
       break; 

      case NotifyCollectionChangedAction.Replace: 
       throw new NotImplementedException("Replace not implemented yet"); 
     } 
    } 

    /// <summary> 
    /// update the visible child in the ItemsHolder 
    /// </summary> 
    /// <param name="e"></param> 
    protected override void OnSelectionChanged(SelectionChangedEventArgs e) 
    { 
     base.OnSelectionChanged(e); 
     UpdateSelectedItem(); 
    } 

    /// <summary> 
    /// generate a ContentPresenter for the selected item 
    /// </summary> 
    void UpdateSelectedItem() 
    { 
     if (_itemsHolder == null) 
     { 
      return; 
     } 

     // generate a ContentPresenter if necessary 
     TabItem item = GetSelectedTabItem(); 
     if (item != null) 
     { 
      CreateChildContentPresenter(item); 
     } 

     // show the right child 
     foreach (ContentPresenter child in _itemsHolder.Children) 
     { 
      child.Visibility = ((child.Tag as TabItem).IsSelected) ? Visibility.Visible : Visibility.Collapsed; 
     } 
    } 

    /// <summary> 
    /// create the child ContentPresenter for the given item (could be data or a TabItem) 
    /// </summary> 
    /// <param name="item"></param> 
    /// <returns></returns> 
    ContentPresenter CreateChildContentPresenter(object item) 
    { 
     if (item == null) 
     { 
      return null; 
     } 

     ContentPresenter cp = FindChildContentPresenter(item); 

     if (cp != null) 
     { 
      return cp; 
     } 

     // the actual child to be added. cp.Tag is a reference to the TabItem 
     cp = new ContentPresenter(); 
     cp.Content = (item is TabItem) ? (item as TabItem).Content : item; 
     cp.ContentTemplate = this.SelectedContentTemplate; 
     cp.ContentTemplateSelector = this.SelectedContentTemplateSelector; 
     cp.ContentStringFormat = this.SelectedContentStringFormat; 
     cp.Visibility = Visibility.Collapsed; 
     cp.Tag = (item is TabItem) ? item : (this.ItemContainerGenerator.ContainerFromItem(item)); 
     _itemsHolder.Children.Add(cp); 
     return cp; 
    } 

    /// <summary> 
    /// Find the CP for the given object. data could be a TabItem or a piece of data 
    /// </summary> 
    /// <param name="data"></param> 
    /// <returns></returns> 
    ContentPresenter FindChildContentPresenter(object data) 
    { 
     if (data is TabItem) 
     { 
      data = (data as TabItem).Content; 
     } 

     if (data == null) 
     { 
      return null; 
     } 

     if (_itemsHolder == null) 
     { 
      return null; 
     } 

     foreach (ContentPresenter cp in _itemsHolder.Children) 
     { 
      if (cp.Content == data) 
      { 
       return cp; 
      } 
     } 

     return null; 
    } 

    /// <summary> 
    /// copied from TabControl; wish it were protected in that class instead of private 
    /// </summary> 
    /// <returns></returns> 
    protected TabItem GetSelectedTabItem() 
    { 
     object selectedItem = base.SelectedItem; 
     if (selectedItem == null) 
     { 
      return null; 
     } 

     if (_deletedObject == selectedItem) 
     { 

     } 

     TabItem item = selectedItem as TabItem; 
     if (item == null) 
     { 
      item = base.ItemContainerGenerator.ContainerFromIndex(base.SelectedIndex) as TabItem; 
     } 
     return item; 
    } 
} 

Шаблон TabControl Я обычно использую выглядит примерно так:

<Style x:Key="TabControlEx_NoHeadersStyle" TargetType="{x:Type local:TabControlEx}"> 
    <Setter Property="SnapsToDevicePixels" Value="true"/> 
    <Setter Property="Template"> 
     <Setter.Value> 
      <ControlTemplate TargetType="{x:Type localControls:TabControlEx}"> 
       <DockPanel> 
        <!-- This is needed to draw TabControls with Bound items --> 
        <StackPanel IsItemsHost="True" Height="0" Width="0" /> 
        <Grid x:Name="PART_ItemsHolder" /> 
       </DockPanel> 
      </ControlTemplate> 
     </Setter.Value> 
    </Setter> 
</Style>