I'm slowly learning MVVM in WPF. I code a GBA game editor. The editor consist of a main window (Editor.xaml) and depending which editor is selected in the menu, I'd like to display the corresponding persistent view (that is not destroyer when switching).
I'm trying to get working the TabControlEx class found in a few posts on SO such as here and here.
However I run into two problem: first, tabControlEx selectedItem does not change (fixed see edit) and second it seems on the TabItem OnMouseOver I lose the View (having a color Background property for the TabItem will switch to white color).
Now I'm pretty sure I'm missing something more or less obvious, but being new to MVVM I don't really know where to look. So here the code breakdown.
TabControlEx.cs
[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;
}
}
My main view:
Editor.xaml
<Window.Resources>
<ResourceDictionary Source="Resources/GlobalDictionary.xaml" />
</Window.Resources>
<DockPanel>
<DockPanel DockPanel.Dock="Top" KeyboardNavigation.TabNavigation="None">
<Menu Height="20" KeyboardNavigation.TabNavigation="Cycle">
<MenuItem Header="{StaticResource MenuFile}">
<MenuItem Header="{StaticResource MenuOpen}" />
<MenuItem Header="{StaticResource MenuSave}" />
<MenuItem Header="{StaticResource MenuExit}" Command="{Binding Path=CloseCommand}" />
</MenuItem>
<MenuItem Header="{StaticResource MenuEditors}">
<MenuItem Header="{StaticResource MenuMonster}" Command="{Binding Path=OpenMonsterEditor}"/>
</MenuItem>
<MenuItem Header="{StaticResource MenuHelp}">
<MenuItem Header="{StaticResource MenuAbout}" />
<MenuItem Header="{StaticResource MenuContact}" />
</MenuItem>
</Menu>
</DockPanel>
<controls:TabControlEx ItemsSource="{Binding AvailableEditors}"
SelectedItem="{Binding CurrentEditor}"
Style="{StaticResource BlankTabControlTemplate}">
</controls:TabControlEx>
</DockPanel>
GlobalDictionary.xaml
<DataTemplate DataType="{x:Type vm:GBARomViewModel}">
<vw:GBARomView />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:MonsterViewModel}">
<vw:MonsterView />
</DataTemplate>
<Style x:Key="BlankTabControlTemplate" TargetType="{x:Type control:TabControlEx}">
<Setter Property="SnapsToDevicePixels" Value="true"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type control: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>
I'm not sure if the following is right, but each view extent TabItem as follow. The first view on startup is shown correctly.
GBARomView.xaml
<TabItem x:Class="FF6AE.View.GBARomView"
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:FF6AE.View"
mc:Ignorable="d"
d:DesignHeight="380" d:DesignWidth="646"
Background="BlueViolet">
<Grid >
</Grid>
</TabItem>
GBARomView.xaml.cs
public partial class GBARomView : TabItem
{
public GBARomView()
{
InitializeComponent();
}
}
Finally my main viewModel:
EDitorViewModel.cs
public class EditorViewModel: ViewModelBase
{
ViewModelBase _currentEditor;
ObservableCollection<ViewModelBase> _availableEditors;
RelayCommand _closeCommand;
RelayCommand _OpenMonsEditorCommand;
public EditorViewModel()
{
base.DisplayName = (string)AppInst.GetResource("EditorName");
_availableEditors = new ObservableCollection<ViewModelBase>();
_availableEditors.Add(new GBARomViewModel());
_availableEditors.Add(new MonsterViewModel());
_availableEditors.CollectionChanged += this.OnAvailableEditorsChanged;
_currentEditor = _availableEditors[0];
_currentEditor.PropertyChanged += this.OnSubEditorChanged;
}
public ViewModelBase CurrentEditor
{
get
{
if (_currentEditor == null)
{
_currentEditor = new GBARomViewModel();
_currentEditor.PropertyChanged += this.OnSubEditorChanged;
}
return _currentEditor;
}
set
{
_currentEditor = value;
// this is the thing I was missing
OnPropertyChanged("CurrentEditor");
}
}
void OnSubEditorChanged(object sender, PropertyChangedEventArgs e)
{
// For future use
}
public ObservableCollection<ViewModelBase> AvailableEditors
{
get
{
return _availableEditors;
}
}
void OnAvailableEditorsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
// For future use
}
public ICommand OpenMonsterEditor
{
get
{
if (_OpenMonsEditorCommand == null)
_OpenMonsEditorCommand = new RelayCommand(param => this.OpenRequestOpen());
return _OpenMonsEditorCommand;
}
}
void OpenRequestOpen()
{
_currentEditor = _availableEditors[1];
}
public ICommand CloseCommand
{
get
{
if (_closeCommand == null)
_closeCommand = new RelayCommand(param => this.OnRequestClose());
return _closeCommand;
}
}
public event EventHandler RequestClose;
void OnRequestClose()
{
EventHandler handler = this.RequestClose;
if (handler != null)
handler(this, EventArgs.Empty);
}
}
So basically I am lost why clicking on the monster editor in the menu does not switch the view though currentEditor value is changed (fixed) and why on mouse over on the tabItem I lose the background testing color of the view (it turns to white). <- still not fixed!
Any help would be appreciated. Thanks in advance.
Edit: I was missing the OnPropertyChanged("CurrentEditor"); in the setter of CurrentEditor.