I've been wrestling with this problem for some time and I haven't found a workable answer, and I'm sure it has a lot to do with my lack of understanding of WPF.
The basic architecture of my program will be similar to Visual Studio, with blocks of tabs that can be arranged various ways. For simplicity, I currently have a single block of tabs that cannot be arranged in any way - the form contains a custom control that contains a tab control.
Now, the content of each tab page will be a single custom control that is a view for a document. The tab pages may have different documents, and each document has a specific custom control and document view model pairing.
My major issue is that I cannot figure out how to reference the tab page's view model in the tab page control's code-behind.
Here is the code for the TabPane - the custom control that is the block of tabs. It includes code I pieced together from various web sites for adding a close button to the tabs (I may add more buttons later).
<UserControl x:Class="MyApp.TabPane"
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:MyApp"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<DockPanel LastChildFill="True" >
<TabControl ItemsSource="{Binding Path=ViewModel.TabPages}" SelectedItem="{Binding Path=ViewModel.ActivePage}" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch">
<TabControl.Resources>
<DataTemplate DataType="{x:Type local:NowPlayingViewModel}" >
<local:NowPlayingControl />
</DataTemplate>
<DataTemplate DataType="{x:Type local:TypeEditorDocumentViewModel}" >
<local:TypeEditorControl />
</DataTemplate>
</TabControl.Resources>
<TabControl.ItemContainerStyle>
<Style TargetType="TabItem">
<Setter Property="HeaderTemplate" >
<Setter.Value>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="0" Height="22">
<TextBlock VerticalAlignment="Center" Text="{Binding Path=Caption}" />
<local:LibraryTabHeaderButton Name="tabPageCloseButton" Width="20" Height="19" Margin="6,0,0,0" Padding="0" HorizontalAlignment="Center" VerticalAlignment="Center"
Focusable="False"
Visibility="{Binding Path=AllowClose, Converter={local:BooleanToVisibilityConverter}}"
Click="tabPageCloseButton_Click">
<Path Data="M1,9 L9,1 M1,1 L9,9" Stroke="Black" StrokeThickness="2" />
</local:LibraryTabHeaderButton>
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</TabControl.ItemContainerStyle>
</TabControl>
</DockPanel>
This is the code-behind for the TabPane. The TabPane is assigned a view model by the application so that the view model can provide a pre-existing set of tabs (there will be some tabs that can only exist in one TabPane and cannot be closed). The close button will need to tell the view model which tab is closing. Any closable tab may be closed, regardless of whether or not the tab is active.
public partial class TabPane : UserControl
{
private TabPaneViewModel viewModel = null;
public TabPane()
{
viewModel = ApplicationManager.GetLibraryViewModel().GetNewTabPaneViewModel();
InitializeComponent();
}
public TabPaneViewModel ViewModel
{
get { return viewModel; }
}
private void tabPageCloseButton_Click(object sender, EventArgs e)
{
//Button button = (Button)sender;
}
The TabPaneViewModel is pretty straight forward. It basicly holds a collection of tab pages.
public class TabPaneViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private readonly int id;
private ObservableCollection<ILibraryDocumentViewModel> tabPages = new ObservableCollection<ILibraryDocumentViewModel>();
private ILibraryDocumentViewModel activePage = null;
public TabPaneViewModel(int id)
{
this.id = id;
tabPages.CollectionChanged += tabPages_CollectionChanged;
}
private void tabPages_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if( !isDisposed && (e.Action == NotifyCollectionChangedAction.Add) )
ActivePage = (ILibraryDocumentViewModel)e.NewItems[0];
}
public ObservableCollection<ILibraryDocumentViewModel> TabPages
{
get { return tabPages; }
}
public ILibraryDocumentViewModel ActivePage
{
get { return activePage; }
set
{
activePage = value;
OnNotifyPropertyChanged("ActivePage");
}
}
private void OnNotifyPropertyChanged(string propertyName)
{
if( !isDisposed && (PropertyChanged != null) )
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
The LibraryTabHeaderButton is a simple custom control with a click event. I've found that this header button has no idea which tab it is associated with, so somehow I need to tell it. I have not figured out how to do this yet.
public partial class LibraryTabHeaderButton : UserControl
{
public event EventHandler Click;
public LibraryTabHeaderButton()
{
InitializeComponent();
}
public int TabPageId { get; set; }
private void OnClick(object sender, RoutedEventArgs args)
{
LibraryTabHeaderButtonClickEventArgs newArgs = new LibraryTabHeaderButtonClickEventArgs();
newArgs.TabPageId = TabPageId;
if( Click != null )
Click(sender, newArgs);
}
}
The LibraryViewModel is the source of the documents. It receives an instruction to create or open a document, does so, and adds it to a TabPane.
public class LibraryViewModel
{
private List<TabPaneViewModel> tabPanes = new List<TabPaneViewModel>();
private static int nextTabPaneId = 1;
private TabPaneViewModel activeTabPane = null;
private NowPlayingViewModel nowPlayingViewModel = null;
private static int nextTabPageId = 1;
public TabPaneViewModel GetNewTabPaneViewModel()
{
TabPaneViewModel tabPane = new TabPaneViewModel(nextTabPaneId);
nextTabPaneId++;
tabPanes.Add(tabPane);
if( activeTabPane == null )
activeTabPane = tabPane;
if( nowPlayingViewModel == null )
{
// Show the now playing tab on this pane
nowPlayingViewModel = new NowPlayingViewModel(nextTabPageId);
nextTabPageId++;
tabPane.TabPages.Add(nowPlayingViewModel);
}
return tabPane;
}
public void DisplayDocument(LibraryDocumentType documentType)
{
if( activeTabPane != null )
{
ILibraryDocumentViewModel document = null;
switch( documentType )
{
case LibraryDocumentType.TypeEditor:
document = new TypeEditorDocumentViewModel(nextTabPageId);
nextTabPageId++;
break;
}
activeTabPane.TabPages.Add(document);
}
}
}
Here is the ILibraryDocumentViewModel.
public interface ILibraryDocumentViewModel
{
int TabPageId { get; }
LibraryDocumentType DocumentType { get; }
string Caption { get; }
bool AllowClose { get; }
bool IsChanged { get; }
}
This is part of the TypeEditorControl code-behind. I have tried various methods to access the view model from here, but it is always null.
public partial class TypeEditorControl : UserControl
{
private TypeEditorDocumentViewModel viewModel = null;
public TypeEditorControl()
{
object v = this.ViewModel;
viewModel = DataContext as TypeEditorDocumentViewModel;
InitializeComponent();
viewModel = DataContext as TypeEditorDocumentViewModel;
UpdateEditMode();
}
public TypeEditorDocumentViewModel ViewModel
{
get { return viewModel; }
}
Here is part of the TypeEditorControl xaml.
<UserControl x:Class="MyApp.TypeEditorControl"
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:MyApp"
mc:Ignorable="d"
d:DesignHeight="630">
<Grid Margin="0" Background="#FFF0F0F0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Label x:Name="categoryListLbl" Content="Type Categories" Grid.Column="0" HorizontalAlignment="Left" Margin="10,5,10,0" Grid.Row="0" VerticalAlignment="Top"/>
<ListView x:Name="categoryListLvw" Margin="10,0" Grid.Row="1" Grid.ColumnSpan="2" ItemsSource="{Binding Path=ViewModel.TypeCategories}" SelectionChanged="categoryListLvw_SelectionChanged" SelectionMode="Single" >
<ListView.View>
<GridView>
I think that's enough code... Now, the program runs and I can bring up various documents, but the documents don't display data from a view model.
Because I create the document view model and add it to the TabPages ObservableCollection, I cannot get a reference to the document view model in that document control's code behind. The TabPane automatically generates a TypeEditorControl when I add a TypeEditorViewModel, but I can't seem to access the link between them. My current guess is that I don't have the DataContext properly set in the TypeEditorControl xaml (I've tried no DataContext and one referencing Self, neither worked and I don't know what else to try). Maybe my architecture won't support what I'm trying to do. Maybe I'm asking the wrong question.
I'm at a loss of what to try next. Thank you to anyone who reads this enormous posting.