I have one main view which has a tab control. When a tab is selected, it calls the appropriate view to display. I have a function in view model which has to know which tab was selected to preform an operation. How do I achieve this? How will the view model know which tab is selected?
Quite simply:
<Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication2"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<local:TestViewModel x:Key="MainViewModel"/>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="50"/>
</Grid.RowDefinitions>
<TabControl DataContext="{StaticResource MainViewModel}"
SelectedIndex="{Binding Selected}"
Grid.Row="0"
x:Name="TestTabs">
<TabItem Header="Section 1"/>
<TabItem Header="Section 2"/>
<TabItem Header="Section 3"/>
</TabControl>
<Button Content="Check
Selected Index"
Grid.Row="1"
x:Name="TestButton"
Click="TestButton_OnClick"/>
</Grid>
</Window>
The model is defined here, declaratively, as a data context. The selectedindex property is bound to the model so any time it changes, the propery it is mapped to on the view model will also change
class TestViewModel : INotifyPropertyChanged
{
private int _selected;
public int Selected
{
get { return _selected; }
set
{
_selected = value;
OnPropertyChanged("Selected");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
This implements INotifyPropertyChanged so the view will register with it. In the handler here, I output the value of Selected to show the as you change them.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void TestButton_OnClick(object sender, RoutedEventArgs e)
{
var vm = TestTabs.DataContext as TestViewModel;
MessageBox.Show(string.Format("You selected tab {0}", vm.Selected));
}
}
This gets the viewmodel and then shows us that the properties were in fact updated.
In View, you put SelectedIndex property on TabControl:
xmlns:cal="http://www.caliburnproject.org"
<TabControl cal:Message.Attach="[SelectionChanged] = [OnTabSelectionChanged()]"
SelectedIndex="{Binding SelectedIndexTab}">
<TabItem Header="Tab 1"/>
<TabItem Header="Tab 2"/>
</TabControl>
In ViewModel, you declare a public property name SelectedIndexTab and OnTabSelectionChanged() method to operate.
public int SelectedIndexTab { get; set; }
In this example, I use Caliburn to catch SelectionChange event of TabControl.
You can use the SelectionChanged event provided by the Selector base class. The SelectionChangedEventArgs will contain the newly selected (and deselected) items. Alternatively, you can bind the SelectedItem of the Selector Base class to a property in your ViewModel, and then perform some logic in the setter.
Generally though, it's considered a violation of MVVM to pass view-specific objects to your ViewModels - it tightly couples the UI framework (WPF in this case) to the more generic ViewModel logic. A better route is to put event handlers in your UI code-behind which in turn act on the view-model appropriately, but without passing View objects as parameters.
Here is a simple example.
You should be binding the ItemsSource of the Tab to your ObservableCollection, and that should hold models with information about the tabs that should be created.
Here are the VM and the model which represents a tab page:
public class ViewModel
{
public ObservableCollection<TabItem> Tabs { get; set; }
public ViewModel()
{
Tabs = new ObservableCollection<TabItem>();
Tabs.Add(new TabItem { Header = "One", Content = "One's content" });
Tabs.Add(new TabItem { Header = "Two", Content = "Two's content" });
}
}
public class TabItem
{
public string Header { get; set; }
public string Content { get; set; }
}
Here`s the View and VM binding
<Window x:Class="WpfApplication12.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<ViewModel
xmlns="clr-namespace:WpfApplication12" />
</Window.DataContext>
<TabControl
ItemsSource="{Binding Tabs}">
<TabControl.ItemTemplate>
<!-- this is the header template-->
<DataTemplate>
<TextBlock
Text="{Binding Header}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<!-- this is the body of the TabItem template-->
<DataTemplate>
<----- usercontrol namespace goes here--->
</DataTemplate>
</TabControl.ContentTemplate>
Source:link
Related
I have TabControl that has already define some TabItems on XAML. I need to create new TabItems and add to it.
If I use ItemSource I get an exception Items collection must be empty before using ItemsSource.
The solution I have found so far is to create those TabItems I have already defined on XAML but programmatically on the ViewModel, so I can created the others I really need, but doesn't seems to be a good solution.
Other solution would be to add the TabControl as a property and use the Code-Behind to bind it to the ViewModel, which I would like to avoid.
So, I'm just wondering if there is a way to do this only with XAML and MVVM.
Edit:
ItemSource attempt, which is working.
XAML:
<TabControl Grid.Row="1"
Grid.Column="0"
VerticalAlignment="Stretch"
BorderThickness="0.5"
BorderBrush="Black"
ItemsSource="{Binding Model.TabItems, Mode=TwoWay}">
<!--<TabControl.Items>
</TabControl.Items>-->
</TabControl>
Model
public ObservableCollection<TabItem> TabItems {get;set;}
VM
TabItem tabItem = new TabItem { Content = new DetailedViewModel((MyObject)inCommandParameter) };
Model.TabItems.Add(tabItem);
What you are doing here is NOT MvvM. Idea behind it is to keep parts of the app separate, i.e. Model should NOT return any UI elements. If you want to use this with any other UI framework for example WinForms then it will fail and will require additional work.
What you need is something like this, bear in mind that this is an example and you will need to modify this to comply with your requirements.
Model class:
namespace Model
{
public class Profile
{
public string Name { get; set; }
public static int I { get; set; } = 2;
}
}
After this you will need the ViewModel:
namespace VM
{
public class MainViewModel : BaseViewModel
{
public MainViewModel()
{
ProfilesCollection = new List<Profile>();
for (int i = 0; i < 100; i++)
{
ProfilesCollection.Add(new Profile() {Name = $"Name {i}"});
}
}
private List<Profile> profilesCollection;
public List<Profile> ProfilesCollection
{
get { return profilesCollection; }
set { profilesCollection = value; OnPropertyChanged(); }
}
}
}
Now we have base to work with. After that I assume you know how to add the relevant references in your xaml, but this might be seen by other people so I will include it anyway.
Here is a complete MainWindow.xaml:
<Window x:Class="SO_app.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:vm="clr-namespace:VM;assembly=VM"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:converter="clr-namespace:SO_app.Converters"
xmlns:validation="clr-namespace:SO_app.Validation"
xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
xmlns:local="clr-namespace:SO_app"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:model="clr-namespace:Model;assembly=Model"//reference to my model
mc:Ignorable="d"
Title="MainWindow" Height="452.762" Width="525" Closing="Window_Closing">
<!-- d:DataContext="{d:DesignInstance Type=vm:MainViewModel, IsDesignTimeCreatable=True}" -->
<Window.Resources>
<CollectionViewSource Source="{Binding ProfilesCollection}" x:Key="profiles"/> // this corresponds to our collection in VM
</Window.Resources>
<Window.DataContext>
<vm:MainViewModel/>//Data Context of the Window
</Window.DataContext>
<Window.Background>
<VisualBrush>
<VisualBrush.Visual>
<Rectangle Width="50" Height="50" Fill="ForestGreen"></Rectangle>
</VisualBrush.Visual>
</VisualBrush>
</Window.Background>
<TabControl>
<TabControl.Resources>
<DataTemplate DataType="{x:Type model:Profile}">//this data template will be used by the TabControl
<Grid>
<TextBlock Text="{Binding Name}"/>
</Grid>
</DataTemplate>
</TabControl.Resources>
<TabControl.ItemsSource>
<CompositeCollection>
<TabItem Header="First Item"/>
<TabItem Header="SecondItem"/>
<CollectionContainer Collection="{Binding Source={StaticResource profiles}}"/>
</CompositeCollection>
</TabControl.ItemsSource>
</TabControl>
If you want to add more items then just use Command which would be implemented in VM and just add profile to it and enjoy the show.
This is the scenario: In a UserControl there is TabControl, which loads different views, and a button. Like this image:
Scenario
Button "Save" only can be enabled if fields "Name" and "Owner" are not empty. These fields are in a child view loaded in ItemTab.
This is the XAML (only with 1 TabItem to simplify)
<UserControl
x:Class="Winvet.Desktop.Views.VCliente.DatosCliente"
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"
mc:Ignorable="d"
xmlns:viewModels="clr-namespace:Winvet.Desktop.ViewModels.VMCliente"
xmlns:views="clr-namespace:Winvet.Desktop.Views.VCliente">
<Grid Margin="10 5 10 10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="7*"/>
<ColumnDefinition Width="3*"/>
</Grid.ColumnDefinitions>
<TabControl Grid.Column="0" Name="TabDatosCliente" ItemsSource="{Binding ItemsTabDatosCliente}" SelectedIndex="0">
<TabControl.Resources>
<DataTemplate DataType="{x:Type viewModels:DatosClienteGeneralViewModel}">
<views:DatosClienteGeneral/>
</DataTemplate>
</TabControl.Resources>
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Header}" />
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
<StackPanel Grid.Column="1" Orientation="Horizontal">
<Button VerticalAlignment="Bottom" Command="{ I want to bind this }">Guardar</Button>
</StackPanel>
</Grid>
</UserControl>
And this is the ViewModel (only 1 TabItem to simplify)
using System.Collections.ObjectModel;
using Winvet.Desktop.Common;
namespace Winvet.Desktop.ViewModels.VMCliente
{
public class DatosClienteViewModel: ViewModelBase
{
public ObservableCollection<ViewModelBase> ItemsTabDatosCliente { get; private set; }
public DatosClienteViewModel()
{
ItemsTabDatosCliente = new ObservableCollection<ViewModelBase>
{
new DatosClienteGeneralViewModel()
};
}
}
}
I wan't to create a Command which checks if those two child view fields are not empty and enables button. How can I do it?
Routed commands burrow and bubble through the entire interface, so as long as you are in the same visual branch as the item raising the event, then you can handle it anywhere.
so in your VIEW
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
CommandBindings.Add(new CommandBinding(command, execute, canExecute));
}
private void canExecute(object sender, CanExecuteRoutedEventArgs e)
{
throw new NotImplementedException();
//at this point you can pass it to your ViewModel
}
private void execute(object sender, ExecutedRoutedEventArgs e)
{
throw new NotImplementedException();
//at this point you can pass it to your ViewModel
}
}
where command is the RoutedCommand that is set on your buttons Command Property
I'm struggling with update issue. I have tab control with the listbox binded to an Observable Collection
ListBox HorizontalAlignment="Center" Height="450" VerticalAlignment="Top" Width="250"
x:Name="LbxMenu" Background="{x:Null}" BorderBrush="{x:Null}"
ItemsSource="{Binding TestListsNames}" FontFamily="Segoe UI Semilight" FontSize="18"/>
view model:
private ObservableCollection<string> _testListsName;
public ObservableCollection<string> TestListsNames
{
get { return _testListsName; }
set{ _testListsName = value; }
}
After inserting entity to database there is an event which invokes TestListInitialize method in my ViewModel which should refresh collection and it works as I can see it in debugger. But listbox doesn't refresh and I have to restart application to see changes.
It worked great when it was in separate window but when I changed ui to tab control it doesn't.
Update function:
private void TestListNamesInitialize()
{
TestListsNames = db.GetTestListNamesFromDatabase();
if (TestListsNames.Count != 0) CanLoad = true;
}
Initial Window:
<Controls:MetroWindow
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Test.View.InitialWindow"
xmlns:Controls="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
xmlns:tabdata="clr-namespace:Test.View.TabItems"
Title="Testownik" Height="600" Width="900" ShowTitleBar="True" ResizeMode="NoResize" Icon="../GraphicResources/Icon.ico">
<Controls:MetroWindow.RightWindowCommands>
<Controls:WindowCommands>
<Button Content="settings" />
<Button>
<StackPanel Orientation="Horizontal">
<TextBlock Margin="4 0 0 0"
VerticalAlignment="Center"
Text="about" />
</StackPanel>
</Button>
</Controls:WindowCommands>
</Controls:MetroWindow.RightWindowCommands>
<Controls:MetroAnimatedTabControl x:Name ="MainTabControl">
<TabItem Header="Learn" Width="280">
<tabdata:LearnTabItem/>
</TabItem>
<TabItem Header="Database" Width="280">
<tabdata:DatabaseTabItem/>
</TabItem>
<TabItem Header="Statistics" Width="299">
<tabdata:StatisticsTabItem/>
</TabItem>
</Controls:MetroAnimatedTabControl>
Code behind:
public partial class InitialWindow : MetroWindow
{
InitialWindowViewModel viewModel=new InitialWindowViewModel();
public InitialWindow()
{
InitializeComponent();
DataContext = viewModel;
}
}
}
DatabaseTabItem:
<UserControl x:Class="Test.View.TabItems.DatabaseTabItem"
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:Controls="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
xmlns:tabData="clr-namespace:Test.View.TabItems"
Height="500" Width="900" Background="White" BorderBrush="Transparent">
<UserControl.Resources>
</UserControl.Resources>
<Grid>
<Controls:MetroAnimatedTabControl x:Name ="DatabaseTabControl" Grid.Column="0" TabStripPlacement="Left" >
<TabItem Header="Choose" Width="250" >
<tabData:ChooseFromDbTabItem/>
</TabItem>
<TabItem Header="Add" Width="250">
<tabData:AddToDbTabItem/>
</TabItem>
<TabItem Header="Remove" Width="250">
<tabData:DeleteFromDbTabItem/>
</TabItem>
</Controls:MetroAnimatedTabControl>
</Grid>
code behind:
DatabaseViewModel vm = new DatabaseViewModel();
public DatabaseTabItem()
{
InitializeComponent();
DataContext = vm;
}
}
ChooseFromDbTabItem:
<UserControl x:Class="Test.View.TabItems.ChooseFromDbTabItem"
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:Test.View.TabItems"
xmlns:Controls="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
mc:Ignorable="d"
d:DesignHeight="500" d:DesignWidth="650" Background="White" BorderBrush="Transparent">
<Grid>
<ListBox HorizontalAlignment="Center" Height="450" VerticalAlignment="Top" Width="250"
x:Name="LbxMenu" Background="{x:Null}" BorderBrush="{x:Null}"
ItemsSource="{Binding TestListsNames}" FontFamily="Segoe UI Semilight" FontSize="18"/>
</Grid>
code behind:
public partial class ChooseFromDbTabItem : UserControl
{
public ChooseFromDbTabItem()
{
InitializeComponent();
}
}
You have to rise PropertyChanged event for the reason you change your entire collection and not a single item (if you changed a single item that was updated through the Observable).
private ObservableCollection<string> _testListsName;
public ObservableCollection<string> TestListsNames
{
get { return _testListsName; }
set
{
if (_testListsName != value)
{
_testListsName = value;
NotifyPropertyChanged("TestListsNames");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string property)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
You are not raising the PropertyChanged event when replacing the list using the property setter. Generelly, try to make collection properties readonly to reduce the risk for these sort of errors. Instead, clear the list and repopulate it. This will make sure that the view is notified about any changes.
public class ViewModel
{
private readonly ObservableCollection<string> _testListsName;
public ObservableCollection<string> TestListsNames
{
get { return _testListsName; }
}
private void TestListNamesInitialize()
{
_testListsName.Clear();
foreach(string name in db.GetTestListNamesFromDatabase())
{
_testListsName.Add(name);
}
if (_testListsNames.Count != 0) CanLoad = true;
}
}
However, note that this will raise changed events on each item using the .Add() call. See here: Can I somehow temporarily disable WPF data binding changes?
Edit: from your updated code. It can also be seen that you do not set the DataContext on your ChooseFromDbTabItem. You need to bind the DataContext property to the view model that exposes the collection:
<TabItem Header="Choose" Width="250" >
<tabData:ChooseFromDbTabItem DataContext="{Binding}" />
</TabItem>
I have recently started with XAML and WPF. I just created a new project in wpf and add the below XAML code. But...none of the items which are added inside "Listbox.ItemTemplate" or "ListView.ItemTemplate" for that matter show up in designer window. what am i doing wrong? This is a fresh project and so no code-behind stuff added yet. i scratched my head for 15 mins with this but with no success. Please help
<Window x:Class="WpfApplication3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid Margin="10">
<ListBox Margin="10">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Name: " />
<TextBlock Text="Age: " />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>
You should bind your ListBox or entire Window to some DataContext (usually this is viewmodel with the data you need to display) or specify items of the list explicitly.
In your snippet you specified only an item template, not the items itself.
The example of XAML-defined items (simple strings):
<Window x:Class="WpfApplication3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid Margin="10">
<ListBox Margin="10">
<ListBox.Items>
<ListBoxItem>123</ListBoxItem>
<ListBoxItem>456</ListBoxItem>
</ListBox.Items>
</ListBox>
</Grid>
</Window>
The example with DataContext and Bindings.
XAML:
<Window x:Class="WpfApplication3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid Margin="10">
<ListBox Margin="10" ItemsSource="{Binding Path=Persons}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Label>Name:</Label><TextBlock VerticalAlignment="Center" Text="{Binding Path=Name}" />
<Label>Age:</Label><TextBlock VerticalAlignment="Center" Text="{Binding Path=Age}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>
Codebehind:
namespace WpfApplication3
{
public class PersonViewModel
{
public PersonViewModel(string name, int age)
{
this.name = name;
this.age = age;
}
public string Name
{
get { return name; }
}
private string name;
public int Age
{
get { return age; }
}
private int age;
}
public class MainViewModel
{
public MainViewModel()
{
persons = new ObservableCollection<PersonViewModel>()
{
new PersonViewModel("Lez", 146),
new PersonViewModel("Binja", 158),
new PersonViewModel("Rufus the Destroyer", 9000)
};
}
public ObservableCollection<PersonViewModel> Persons
{
get { return persons; }
}
private ObservableCollection<PersonViewModel> persons;
}
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainViewModel();
}
}
}
IMPORTANT: Don't forget to properly implement INotifyPropertyChanged in case of mutable properties of viewmodels (for example, if you will have setters for "Name" and "Age" properties of PersonViewModel).
You don't see any items because your ListBox doesn't contain any data in the designer view. To populate it, a list should be binded to the property "ItemsSource" of your ListBox, or adding datas directly to the property "Items" (XAML or code behind).
If you want to show items in the designer view properly, you should have a ViewModel binding to the DataContext of your page and create a "Sample data" file (via Blend for example).
Item Templates are basically used to show customize data. Just Use simple string items first. It will show in list box.
Basically I have in my MainViewModel.cs:
ObservableCollection<TabItem> MyTabs { get; private set; }
However, I need to somehow be able to not only create the tabs, but have the tabs content be loaded and linked to their appropriate viewmodels while maintaining MVVM.
Basically, how can I get a usercontrol to be loaded as the content of a tabitem AND have that usercontrol wired up to an appropriate viewmodel. The part that makes this difficult is the ViewModel is not supposed to construct the actual view items, right? Or can it?
Basically, would this be MVVM appropriate:
UserControl address = new AddressControl();
NotificationObject vm = new AddressViewModel();
address.DataContext = vm;
MyTabs[0] = new TabItem()
{
Content = address;
}
I only ask because well, i'm constructing a View (AddressControl) from within a ViewModel, which to me sounds like a MVVM no-no.
This isn't MVVM. You should not be creating UI elements in your view model.
You should be binding the ItemsSource of the Tab to your ObservableCollection, and that should hold models with information about the tabs that should be created.
Here are the VM and the model which represents a tab page:
public sealed class ViewModel
{
public ObservableCollection<TabItem> Tabs {get;set;}
public ViewModel()
{
Tabs = new ObservableCollection<TabItem>();
Tabs.Add(new TabItem { Header = "One", Content = "One's content" });
Tabs.Add(new TabItem { Header = "Two", Content = "Two's content" });
}
}
public sealed class TabItem
{
public string Header { get; set; }
public string Content { get; set; }
}
And here is how the bindings look in the window:
<Window x:Class="WpfApplication12.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<ViewModel
xmlns="clr-namespace:WpfApplication12" />
</Window.DataContext>
<TabControl
ItemsSource="{Binding Tabs}">
<TabControl.ItemTemplate>
<!-- this is the header template-->
<DataTemplate>
<TextBlock
Text="{Binding Header}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<!-- this is the body of the TabItem template-->
<DataTemplate>
<TextBlock
Text="{Binding Content}" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Window>
(Note, if you want different stuff in different tabs, use DataTemplates. Either each tab's view model should be its own class, or create a custom DataTemplateSelector to pick the correct template.)
A UserControl inside the data template:
<TabControl
ItemsSource="{Binding Tabs}">
<TabControl.ItemTemplate>
<!-- this is the header template-->
<DataTemplate>
<TextBlock
Text="{Binding Header}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<!-- this is the body of the TabItem template-->
<DataTemplate>
<MyUserControl xmlns="clr-namespace:WpfApplication12" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
In Prism you usually make the tab control a region so that you don't have to take control over the bound tab page collection.
<TabControl
x:Name="MainRegionHost"
Regions:RegionManager.RegionName="MainRegion"
/>
Now the views can be added via registering itself into the region MainRegion:
RegionManager.RegisterViewWithRegion( "MainRegion",
( ) => Container.Resolve<IMyViewModel>( ).View );
And here you can see a speciality of Prism. The View is instanciated by the ViewModel. In my case I resolve the ViewModel throught a Inversion of Control container (e.g. Unity or MEF). The ViewModel gets the View injected via constructor injection and sets itself as the View's data context.
The alternative is to register the view's type into the region controller:
RegionManager.RegisterViewWithRegion( "MainRegion", typeof( MyView ) );
Using this approach allows you to create the views later during runtime, e.g. by a controller:
IRegion region = this._regionManager.Regions["MainRegion"];
object mainView = region.GetView( MainViewName );
if ( mainView == null )
{
var view = _container.ResolveSessionRelatedView<MainView>( );
region.Add( view, MainViewName );
}
Because you have registered the View's type, the view is placed into the correct region.
I have a Converter to decouple the UI and ViewModel,thats the point below:
<TabControl.ContentTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding Tab,Converter={StaticResource TabItemConverter}"/>
</DataTemplate>
</TabControl.ContentTemplate>
The Tab is a enum in my TabItemViewModel and the TabItemConverter convert it to the real UI.
In the TabItemConverter,just get the value and Return a usercontrol you need.
My solution uses ViewModels directly, so I think it might be useful to someone:
First, I bind the Views to the ViewModels in the App.xaml file:
<Application.Resources>
<DataTemplate DataType="{x:Type local:ViewModel1}">
<local:View1/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:ViewModel2}">
<local:View2/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:ViewModel3}">
<local:View3/>
</DataTemplate>
</Application.Resources>
The MainViewModel looks like this:
public class MainViewModel : ObservableObject
{
private ObservableCollection<ViewModelBase> _viewModels = new ObservableCollection<ViewModelBase>();
public ObservableCollection<ViewModelBase> ViewModels
{
get { return _viewModels; }
set
{
_viewModels = value;
OnPropertyChanged();
}
}
private ViewModelBase _currentViewModel;
public ViewModelBase CurrentViewModel
{
get { return _currentViewModel; }
set
{
_currentViewModel = value;
OnPropertyChanged();
}
}
private ICommand _closeTabCommand;
public ICommand CloseTabCommand => _closeTabCommand ?? (_closeTabCommand = new RelayCommand(p => closeTab()));
private void closeTab()
{
ViewModels.Remove(CurrentViewModel);
CurrentViewModel = ViewModels.LastOrDefault();
}
private ICommand _openTabCommand;
public ICommand OpenTabCommand => _openTabCommand ?? (_openTabCommand = new RelayCommand(p => openTab(p)));
private void openTab(object selectedItem)
{
Type viewModelType;
switch (selectedItem)
{
case "1":
{
viewModelType = typeof(ViewModel1);
break;
}
case "2":
{
viewModelType = typeof(ViewModel2);
break;
}
default:
throw new Exception("Item " + selectedItem + " not set.");
}
displayVM(viewModelType);
}
private void displayVM(Type viewModelType)
{
if (!_viewModels.Where(vm => vm.GetType() == viewModelType).Any())
{
ViewModels.Add((ViewModelBase)Activator.CreateInstance(viewModelType));
}
CurrentViewModel = ViewModels.Single(vm => vm.GetType() == viewModelType);
}
}
}
MainWindow.XAML:
<Window.DataContext>
<local:MainWindowViewModel x:Name="vm"/>
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Menu Grid.Row="0">
<MenuItem Header="1" Command="{Binding OpenTabCommand}" CommandParameter="1"/>
<MenuItem Header="2" Command="{Binding OpenTabCommand}" CommandParameter="2"/>
<MenuItem Header="3" Command="{Binding OpenTabCommand}" CommandParameter="3"/>
</Menu>
<TabControl Grid.Row="1" ItemsSource="{Binding ViewModels}" SelectedItem="{Binding CurrentViewModel}">
<TabControl.ItemTemplate>
<DataTemplate DataType="{x:Type MVVMLib:ViewModelBase}">
<TextBlock Text="{Binding Title}">
<Hyperlink Command="{Binding RelativeSource={RelativeSource AncestorType=Window, Mode=FindAncestor}, Path=DataContext.CloseWindowCommand}">X</Hyperlink>
</TextBlock>
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
</Grid>
I translated some parts to make it easier to understand, there might be some typos.