That's how I defined TabControl:
<TabControl ItemsSource="{Binding OpenedProjects, UpdateSourceTrigger=PropertyChanged}"
SelectedItem="{Binding SelectedProject, Mode=OneWay}">
<!-- headers -->
<!-- header definition is unimportant for this question -->
<!-- content -->
<TabControl.ContentTemplate>
<DataTemplate>
<local:ProjectView />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
And these are the two methods I have defined in my Module class, that are used to register and use views:
protected override void _initializeViews() {
_container.RegisterType<MainMenuView>();
_container.RegisterType<ProjectsView>();
_container.RegisterType<ProjectView>();
_container.RegisterType<ContentView>();
}
protected override void _initializeRegions() {
IRegion menuRegion = _regionManager.Regions[RegionNames.MainMenuRegion];
IRegion projectsRegion = _regionManager.Regions[RegionNames.ProjectsRegion];
IRegion contentRegion = _regionManager.Regions[RegionNames.ContentRegion];
menuRegion.Add(_container.Resolve<MainMenuView>());
projectsRegion.Add(_container.Resolve<ProjectsView>());
contentRegion.Add(_container.Resolve<ContentView>());
}
And the View constructor:
public ProjectView(ProjectsViewModel vm) {
InitializeComponent();
DataContext = vm;
}
What I want to achieve is to inject ProjectView into TabControl's content area. Obviously, currently it doesn't work because of the ViewModel argument in the above constructor. How can I create this functionality, the PRISM way?
EDIT:
I found this: How to inject views into TabControl using Prism? however if I do the same as the author of that question, I'm getting:
System.InvalidOperationException: ItemsControl's ItemsSource property is not empty.
You TabControl didn't have a region so you can't inject something into your TabControl. Otherwise you only use simple MVVM to inject something into your view.
To use Prism to inject something in your TabControl. You only need this line:
<TabControl prism:RegionManager.RegionName="TabRegion"/>
And then you can inject something very easy into your View.
_regionManager.RequestNavigate("TabRegion", new Uri("ProjectView", UriKind.Relative));
Before that you have to add the View to your Containier with:
UnityContainer.RegisterType<object, ProjectView>("ProjectView");
To add the Headertext you can easy changed the Style of the TabItem and bind the Header to the ViewModel from the ProjectView:
<UserControl.Resources>
<Style TargetType="TabItem">
<Setter Property="Header" Value="{Binding DataContext.Name}" />
</Style>
</UserControl.Resources>
I hope that was the answer you looking for^^
The answer from #ascholz help me to implement this. Although the last step didn't work for me:
<UserControl.Resources>
<Style TargetType="TabItem">
<Setter Property="Header" Value="{Binding DataContext.Name}" />
</Style>
</UserControl.Resources>
What i did instead was:
1 - Create a Tab Control with a prism region (Inside MainWindows in my case).
<TabControl prism:RegionManager.RegionName="TabRegion"/>
2 - Create a "user control" of type TabItem (NewTabView) that holds the tab views. Note that i'm binding the Header here. The idea here would be to add a region here as well inside the grid (for the content of the tab), or to make every control that needs to be inside the tab a child of TabItem.
<TabItem
x:Class="Client.WPF.Views.NewTab"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True"
Header="{Binding Title}">
<Grid>
<Button Content="{Binding RandomNumber}"/>
</Grid>
</TabItem>
///The code behind should inherit from TabItem as well
public partial class NewTab : TabItem
///The viewmodel has a "Title" property
private string _title = "New Tab";
public string Title
{
get { return _title; }
set { SetProperty(ref _title, value); }
}
4 - Finally, i navigate to the NewTab like this (MainWindowViewModel code)
public DelegateCommand NewTab { get; }
public MainWindowViewModel(IRegionManager regionManager, IEventAggregator eventAggregato)
{
this.regionManager = regionManager;
this.eventAggregator = eventAggregator;
NewTab = new DelegateCommand(NewTabAction);
}
private void NewTabAction()
{
regionManager.RequestNavigate("TabRegion", "NewTab");
}
5 - As an added bonus, if you want to allow more than 1 instance of the tab, you can do something like this on the view model (NewTabViewModel).
///First add the IConfirmNavigationRequest interface
public class NewTabViewModel : BindableBase, IConfirmNavigationRequest
///...
///Then the implementation should look like this
public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback)
{
continuationCallback(true);///Will allow multiple instances (tabs) of this view
}
public void OnNavigatedTo(NavigationContext navigationContext)
{
}
public bool IsNavigationTarget(NavigationContext navigationContext)
{
return false;
}
public void OnNavigatedFrom(NavigationContext navigationContext)
{
}
Or you could also add views directly to the region. Although you would need to resolve views using the IContainerProvider. Something like this:
var view = containerProvider.Resolve<NewTab>();
regionManager.Regions["TabRegion"].Add(view);
Related
How can I duplicate the effect of the following binding:
<TextBlock Text="{x:Bind viewmodels:ShellViewModel.TheShellViewModel.CurrentCaptionButtonPathSize, Mode=OneWay}"/>
with a Binding in WinUI 3 (v1.1.5)?
TheShellViewModel is a static ObservableProperty in the ShellViewModel class that refers to the singleton instance of the ShellViewModel class in the application. CurrentCaptionButtonPathSize is an ObservableProperty in that instance.
Using a DataContext or creating a property in the code-behind class enables access with a {Binding ...} but neither of these is available in a ControlTemplate (x:Bind works there if set up correctly).
I thought
<TextBlock Text="{Binding TheShellViewModel.CurrentCaptionButtonPathSize, Source=viewmodels:ShellViewModel}"/>
or something similar would work, but it doesn't. What is the proper form for Path and Source in this case, please?
==== Additional Code (added) ====
public partial class ShellViewModel : ObservableRecipient
{
public static ShellViewModel TheShellViewModel { get; private set; }
public ShellViewModel(INavigationService navigationService, INavigationViewService navigationViewService)
{
// .ctor code
TheShellViewModel = this;
}
private double _CurrentCaptionButtonPathSize;
public double CurrentCaptionButtonPathSize
{
get => _CurrentCaptionButtonPathSize;
set => SetProperty(ref _CurrentCaptionButtonPathSize, value);
}
}
<ResourceDictionary
xmlns:models="using:PFSI.ViewModels">
<ControlTemplate x:Name="CaptionButtonTemplate" TargetType="Button">
<Border>
<!-- Layout and VisualState infomation -->
<TextBlock x:Name="cbText"
Text="{x:Bind viewmodels:ShellViewModel.TheShellViewModel.CurrentCaptionButtonPathSize, Mode=OneWay}"/>
</Border>
</ControlTemplate>
</ResourceDictionary>
To make the x:Bind on cbText work:
The {x:Bind} markup extension depends on code generation, so it needs a code-behind file containing a constructor that calls InitializeComponent (to initialize the generated code). You re-use the resource dictionary by instantiating its type (so that InitializeComponent is called) instead of referencing its filename.
(see: Data Binding in Depth) which is easily done (not entirely shown, here). Binding in code is straightforward using OnApplyTemplate() on a class derived from Button. Neither of these solutions are as clean and obvious as using some sort of out-of-scope reference for a XAML Binding.
I feel certain I've done this before but, in a Senior moment, I can't remember how. I ultimately went with the OnApplyTemplate() solution in the interests of moving on (and OnApplyTemplate() has some other benefits, too).
Thanks for any advice.
AFAIK, you can't use x:Static in UWP. One way to do this might be accessing the static class via a property.
This works:
MainWindow.xaml.cs
public sealed partial class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
}
public ShellViewModel CodeBehindShellViewModel { get => ShellViewModel.TheShellViewModel; }
}
MainWindow.xaml
<Window
x:Class="Miscellaneous.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:viewmodels="using:Miscellaneous.ViewModels"
mc:Ignorable="d">
<StackPanel>
<StackPanel.Resources>
<Style
x:Key="CustomButton"
TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border>
<TextBlock
x:Name="cbText"
Foreground="SkyBlue"
Text="{Binding ElementName=ThisWindow, Path=CodeBehindShellViewModel.CurrentCaptionButtonPathSize}" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</StackPanel.Resources>
<Button Style="{StaticResource CustomButton}" />
<Button
Command="{x:Bind viewmodels:ShellViewModel.TheShellViewModel.DoublePathSizeCommand}"
Content="Double" />
</StackPanel>
</Window>
ShellViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace Miscellaneous.ViewModels;
public partial class ShellViewModel : ObservableRecipient
{
// You need to instantiate this.
// In this case, I just instantiated it here.
public static ShellViewModel TheShellViewModel { get; private set; } = new();
public ShellViewModel(/*INavigationService navigationService, INavigationViewService navigationViewService*/)
{
// .ctor code
TheShellViewModel = this;
CurrentCaptionButtonPathSize = 123.45;
}
// Assuming you are using the "CommunityToolkit.Mvvm",
// you can use the "ObservableProperty" attribute and
// auto-generate a "CurrentCaptionButtonPathSize" property.
[ObservableProperty]
private double _currentCaptionButtonPathSize;
//public double CurrentCaptionButtonPathSize
//{
// get => _CurrentCaptionButtonPathSize;
// set => SetProperty(ref _CurrentCaptionButtonPathSize, value);
//}
// This is just a test code to check if the "CurrentCaptionButtonPathSize" works.
// The "RelayCommand" will auto-generate a "DoublePathSizeCommand" command for you.
[RelayCommand]
private void DoublePathSize() => CurrentCaptionButtonPathSize *= 2.0;
}
Trying to create a TabControl Region inside another Region. The TabControl has a set number of Views that will be added to it, with their own respective ViewModels.
But either the View doesn't show up, the tabitem doesn't show up with only one View displayed instead, or I get the following error:
System.ArgumentException: 'This RegionManager does not contain a Region with the name 'ParentTabRegion'. (Parameter 'regionName')'
MainMenuView:
<Grid>
<ContentControl prism:RegionManager.RegionName="ContentRegion" />
</Grid>
MainMenuViewModel:
public class MainMenuViewModel : BindableBase
{
private readonly IRegionManager _regionManger;
public MainMenuViewModel(IRegionManager regionManager)
{
_regionManger = regionManager;
_regionManger.RequestNavigate("ContentRegion", "ParentView");
}
}
ParentView:
<Grid>
<TabControl prism:RegionManager.RegionName="ParentTabRegion" />
</Grid>
ParentViewModel:
public class ParentViewModel : BindableBase
{
private readonly IRegionManager _regionManger;
private Child1View _tab1 = new Child1View();
private Child1View Tab1
{
get { return _tab1; }
set { SetProperty(ref _tab1, value); }
}
private Child2View _tab2 = new Child2View();
private Child2View Tab2
{
get { return _tab2; }
set { SetProperty(ref _tab2, value); }
}
public ParentViewModel(IRegionManager regionManger)
{
_regionManger = regionManger;
// Gives 'This RegionManager does not contain a Region with the name 'GeneralDataTabRegion'. (Parameter 'regionName')' error
_regionManger.AddToRegion("ParentTabRegion", typeof(Child1View));
_regionManger.AddToRegion("ParentTabRegion", typeof(Child2View));
//I've also tried the following
// Same error as above
// _regionManger.Regions["ParentTabRegion"].Add(typeof(Tab1View));
// _regionManger.Regions["ParentTabRegion"].Add(typeof(Tab2View));
// Same error as above
// _regionManger.AddToRegion("ParentTabRegion", Tab1);
// _regionManger.AddToRegion("ParentTabRegion", Tab2);
// Only the last registered view is displayed
// _regionManger.RegisterViewWithRegion("ParentTabRegion", typeof(Tab1));
// _regionManger.RegisterViewWithRegion("ParentTabRegion", typeof(Tab2));
}
}
I also have the prism namespace in all the views:
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True"
Maybe I'm not registering the ParentTabRegion somehow? But I don't have to register the other regions and they seem to just work out of the box.
Let me know if you know what I'm doing wrong or if there is something I'm missing. Thank you.
I would just comment but can't due to low reputation. Anyway..
Check this post
Prism 7 throws and exception when working with nested views
As stated in the comments: "the problem is about how to inject scope region in ViewModel"
This video from Brian should help you with the issue.
https://app.pluralsight.com/library/courses/prism-mastering-tabcontrol
I tested some other things out. Since I don't need dynamic tabs, I found this to be the cleanest solution using Prism:
Parent ViewModel:
public ParentViewModel(IRegionManager regionManager)
{
_regionManager = regionManager;
_regionManager.RegisterViewWithRegion("ChildRegion", typeof(Child1View));
_regionManager.RegisterViewWithRegion("ChildRegion", typeof(Child2View));
}
Parent View:
<UserControl.Resources>
<Style TargetType="TabItem">
<Setter Property="Header"
Value="{Binding DataContext.Title}"/>
</Style>
</UserControl.Resources>
<Grid>
<TabControl prism:RegionManager.RegionName="ChildRegion" />
</Grid>
I ended up doing this a bit differently since I don't really need to dynamically add Tabs.
So what I ended up doing was just adding all the ViewModels to an ObservableCollection of BindableBase. Then I just added them to the view using a DataTemplate.
Parent ViewModel:
private ObservableCollection <BindableBase> _childTabs;
public ObservableCollection <BindableBase> ChildTabs
{
get { return _childTabs; }
set { _childTabs = value; }
}
public ParentViewModel()
{
ChildTabs = new ObservableCollection <BindableBase> {
new Child1ViewModel(),
new Child2ViewModel()
};
}
Parent View:
<TabControl ItemsSource="{Binding ChildTabs}"
SelectedIndex="0">
<TabControl.Resources>
<DataTemplate DataType="{x:Type vm:Child1ViewModel}">
<view:Child1 />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:Child2ViewModel}">
<view:Child2 />
</DataTemplate>
</DataTemplate>
</TabControl.Resources>
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Title}" />
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
I still feel like I'm doing something wrong though, this doesn't feel like MVVM to me...
I'd like to create an app, containing the main menu (ribbonmenu) and different usercontrols, each assigned to an own ViewModel.
I was told to not implement classic events in code-behind but to use commands. So far, everything fine, commands for needed methods are implemented.
In my previous approach I "loaded" the UserControl, by assigning the corresponding ViewModel to a ContentControl, that loaded the UserControl, that was assigned to the ViewModel in MainWindow.Resource.
My last approach, simplified with a button instead of a menu:
<Window.Resources>
<DataTemplate x:Name="settingsViewTemplate" DataType="{x:Type viewmodels:SettingsViewModel}">
<views:SettingsView DataContext="{Binding SettingsVM, Source={StaticResource Locator}}"/>
</DataTemplate>
<DataTemplate x:Name="projectsViewTemplate" DataType="{x:Type viewmodels:ProjectViewModel}">
<views:ProjectView DataContext="{Binding ProjectVM, Source={StaticResource Locator}}"/>
</DataTemplate>
</Window.Resources>
<StackPanel>
<Button Content="Load Settings" Height="20" Margin="20 20 20 0" Click="ShowSettings"/>
<ContentControl Margin="5" Height="100" Content="{Binding}"/>
</StackPanel>
simplified code-behind:
public SettingsViewModel settingsViewModel;
public MainWindow()
{
InitializeComponent();
settingsViewModel = new SettingsViewModel();
}
private void ShowSettings(object sender, RoutedEventArgs e)
{
DataContext = settingsViewModel;
}
How can I load a UserControl, using ViewModel commands?
Don't use code-behind to handle view models. A View model should handle view models. Generally the same view model that implements the commands.
First create a main view model for the MainWindow as data source. This view model will also handle the switching between the views. It's recommended to let all page view models implement a common base type e.g. IPage.
Also you don't need any locator for this scenario. The views inside the DataTemplate will automatically have their DataContext set to the data type that maps to the DataTemplate. SettingsView will automatically have SetingsViewModel as the DataContext. If this would be the wrong context, then your model design is wrong.
IPage.cs
interface IPage : INotifyPropertyChanged
{
string PageTitel { get; set; }
}
SettingsViewModel.cs
class SettingsViewModel : IPage
{
...
}
ProjectViewModel.cs
class ProjectViewModel : IPage
{
...
}
PageName.cs
public enum PageName
{
Undefined = 0, SettingsPage, ProjectPage
}
MainViewModel.cs
An implementation of RelayCommand can be found at
Microsoft Docs: Patterns - WPF Apps With The Model-View-ViewModel Design Pattern - Relaying Command Logic
class MainViewModel : INotifyPropertyChanged
{
public ICommand SelectPageCommand => new RelayCommand(SelectPage);
public Dictionary<PageName, IPage> Pages { get; }
private IPage selectedPage;
public IPage SelectedPage
{
get => this.selectedPage;
set
{
this.selectedPage = value;
OnPropertyChanged();
}
}
public MainViewModel()
{
this.Pages = new Dictionary<PageName, IPage>
{
{ PageName.SettingsPage, new SettingsViewModel() },
{ PageName.ProjectPage, new ProjectViewModel() }
};
this.SelectedPage = this.Pages.First().Value;
}
public void SelectPage(object param)
{
if (param is PageName pageName
&& this.Pages.TryGetValue(pageName, out IPage selectedPage))
{
this.SelectedPage = selectedPage;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
MainWindow.xaml
<Window>
<Window.DataContext>
<MainViewModel />
</Window.DataContext>
<Window.Resources>
<DataTemplate x:Name="settingsViewTemplate" DataType="{x:Type viewmodels:SettingsViewModel}">
<views:SettingsView />
</DataTemplate>
<DataTemplate x:Name="projectsViewTemplate" DataType="{x:Type viewmodels:ProjectViewModel}">
<views:ProjectView />
</DataTemplate>
</Window.Resources>
<StackPanel>
<!-- Content navigation -->
<StackPanel Orientation="Horizontal">
<Button Content="Load Settings"
Command="{Binding SelectPageCommand}"
CommandParameter="{x:Static PageName.SettingsPage}" />
<Button Content="Load Projects"
Command="{Binding SelectPageCommand}"
CommandParameter="{x:Static PageName.ProjectPage}" />
</StackPanel>
<ContentControl Content="{Binding SelectedPage}" />
<StackPanel>
</Window>
The short version:
public class MyViewModel : ViewModel
public MyViewModel()
{
View = new MyUserControlView();
View.DataContext = this; // allow the view to bind to the viewModel.
}
....
public UIElement View {
get; private set;
}
}
And then in XAML:
<ContentControl Content={Binding View} />
There are variations on this theme but that's the basic premise. e.g., if you have a ViewModel that can be bound to multiple views, or ViewModels that have lifetimes longer than their view, you can use a FrameViewModel class like this:
public class FrameViewModel : INotifyProperyChanged; {
public FrameViewModel(IViewModel viewModel; )
{
ViewModel = viewModel;
View = viewModel.CreateView();
View.DataContext = ViewModel;
}
public IViewModel ViewModel { get; set;...}
public UIElement View { get; set; }
}
And then bind THAT into the host XAML with a ContentControl binding to Frame.View.
A more pure approach is to the use the DataTemplateSelector class to instantiate the User Control in a DataTemplate. This is probably the method that WPF designers had in mind for connecting View and ViewModel in WPF. But it ends up spreading the mapping of View and ViewModel across three separate files (the custom C# DataTemplateSelector implementation; widely-separated static resource declaration and ContentControl wrapper in the hosting Window/Page; and the DataTemplate resources themselves which end up in resource files eventually if you have anything but a trivial number of ViewModel/View bindings.
Purists would argue, I suppose, that there's something dirty about having a viewmodel create a view. But there's something far more dirty about code to make DataTemplateSelectors work spread across five files, and inevitable complications with databindings that ensue while trying to tunnel a binding through a DataTemplate.
I have been trying to implement this for a while and haven't been able to do it so far, despite having the feeling that this should be something easy.
The difficulty comes from the fact that I have implemented a WPF application using the MVVM pattern. Now, this is my first attempt at both the pattern and the framework, so it is almost guaranteed that I have made mistakes while trying to follow the MVVM guidelines.
My implementation
I have three Views with their respective ViewModels (wired using Prism's AutoWireViewModel method). The MainView has a TabControl with two TabItems, each of witch contains a Frame container with the Source set to one of the other two Views. The following code is an excerpt of the MainView:
<TabControl Grid.Row="1" Grid.Column="1">
<TabItem Header="Test">
<!--TestView-->
<Frame Source="View1.xaml"/>
</TabItem>
<TabItem Header="Results">
<!--ResultsView-->
<Frame Source="View2.xaml"/>
</TabItem>
</TabControl>
My problem
Every time that someone changes to a specific TabItem, I would like to run a method that updates one of the WPF controls included in that View. The method is already implemented and bound to a Button, but ideally, no button should be necessary, I would like to have some kind of Event to make this happen.
I appreciate all the help in advance.
You could for example handle the Loaded event of the Page to either call a method or invoke a command of the view model once the view has been loaded initially:
public partial class View2 : Page
{
public View2()
{
InitializeComponent();
Loaded += View2_Loaded;
}
private void View2_Loaded(object sender, RoutedEventArgs e)
{
var viewModel = DataContext as ViewModel2;
if (viewModel != null)
viewModel.YourCommand.Execute(null);
Loaded -= View2_Loaded;
}
}
The other option would be handle this in the MainViewModel. You bind the SelectedItem property of the TabControl to a property of the MainViewModel and set this property to an instance of either ViewModel2 or ViewModel2, depending on what kind of view you want to display.
You could then call any method or invoked any command you want on these. But this is another story and then you shouldn't hardcode the TabItems in the view and use Frame elements to display Pages. Please take a look here for an example:
Selecting TabItem in TabControl from ViewModel
Okay, so What I have done is Create a Custom Tab Control. I will write out step by step instructions for this, and then you can add edit to it.
Right click on your solution select add new project
Search For Custom Control Library
High Light the name of the class that comes up, and right click rename it to what ever you want I named it MyTabControl.
Add Prism.Wpf to the new project
Add a reference to the new project to where ever your going to need it. I needed to add to just the main application, but if you have a separate project that only has views then you will need to add it to that too.
Inherit your Custom Control From TabControl Like:
public class MyTabControl : TabControl
You will notice that there is a Themes folder in the project you will need to open the Generic.xaml and edit it. it should look like:
TargetType="{x:Type local:MyTabControl}" BasedOn="{StaticResource {x:Type TabControl}}" for some reason this will not let me show the style tags but they will need to be in there as well
Please review this code I got this from Add A Command To Custom Control
public class MyTabControl : TabControl
{
static MyTabControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MyTabControl), new FrameworkPropertyMetadata(typeof(MyTabControl)));
}
public static readonly DependencyProperty TabChangedCommandProperty = DependencyProperty.Register(
"TabChangedCommand", typeof(ICommand), typeof(MyTabControl),
new PropertyMetadata((ICommand)null,
new PropertyChangedCallback(CommandCallBack)));
private static void CommandCallBack(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var myTabControl = (MyTabControl)d;
myTabControl.HookupCommands((ICommand) e.OldValue, (ICommand) e.NewValue);
}
private void HookupCommands(ICommand oldValue, ICommand newValue)
{
if (oldValue != null)
{
RemoveCommand(oldValue, oldValue);
}
AddCommand(oldValue, oldValue);
}
private void AddCommand(ICommand oldValue, ICommand newCommand)
{
EventHandler handler = new EventHandler(CanExecuteChanged);
var canExecuteChangedHandler = handler;
if (newCommand != null)
{
newCommand.CanExecuteChanged += canExecuteChangedHandler;
}
}
private void CanExecuteChanged(object sender, EventArgs e)
{
if (this.TabChangedCommand != null)
{
if (TabChangedCommand.CanExecute(null))
{
this.IsEnabled = true;
}
else
{
this.IsEnabled = false;
}
}
}
private void RemoveCommand(ICommand oldCommand, ICommand newCommand)
{
EventHandler handler = CanExecuteChanged;
oldCommand.CanExecuteChanged -= handler;
}
public ICommand TabChangedCommand
{
get { return (ICommand) GetValue(TabChangedCommandProperty); }
set { SetValue(TabChangedCommandProperty, value); }
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
this.SelectionChanged += OnSelectionChanged;
}
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (TabChangedCommand != null)
{
TabChangedCommand.Execute(null);
}
}
}
you will need to add the name space in your window or usercontrol like:
xmlns:wpfCustomControlLibrary1="clr-namespace:WpfCustomControlLibrary1;assembly=WpfCustomControlLibrary1"
and here is your control:
<wpfCustomControlLibrary1:MyTabControl TabChangedCommand="{Binding TabChangedCommand}">
<TabItem Header="View A"></TabItem>
<TabItem Header="View B"></TabItem>
</wpfCustomControlLibrary1:MyTabControl>
This is how I'd approach this sort of requirement:
View:
<Window.DataContext>
<local:MainWIndowViewModel/>
</Window.DataContext>
<Grid>
<TabControl Name="tc" ItemsSource="{Binding vms}">
<TabControl.Resources>
<DataTemplate DataType="{x:Type local:uc1vm}">
<local:UserControl1/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:uc2vm}">
<local:UserControl2/>
</DataTemplate>
</TabControl.Resources>
<TabControl.ItemContainerStyle>
<Style TargetType="TabItem">
<Setter Property="Header" Value="{Binding TabHeading}"/>
</Style>
</TabControl.ItemContainerStyle>
</TabControl>
</Grid>
</Window>
When it has a uc1vm it will be templated into usercontrol1 in the view.
I'm binding to a collection of viewmodels which all implement an interface so I know for sure I can cast to that and call a method.
Main viewmodel for window:
private IDoSomething selectedVM;
public IDoSomething SelectedVM
{
get { return selectedVM; }
set
{
selectedVM = value;
selectedVM.doit();
RaisePropertyChanged();
}
}
public ObservableCollection<IDoSomething> vms { get; set; } = new ObservableCollection<IDoSomething>
{ new uc1vm(),
new uc2vm()
};
public MainWIndowViewModel()
{
}
When a tab is selected, the setter for selected item will be passed the new value. Cast that and call the method.
My interface is very simple, since this is just illustrative:
public interface IDoSomething
{
void doit();
}
An example viewmodel, which is again just illustrative and doesn't do much:
public class uc1vm : IDoSomething
{
public string TabHeading { get; set; } = "Uc1";
public void doit()
{
// Your code goes here
}
}
I appreciate all of your input, but I found an alternative solution. Given the information given by #mm8, I took advantage of the Loaded event but in a way that does not require any code in the code behind.
My solution
In the View which I would like to give this ability to execute a method every time the user selects the TabItem that contains it, I added the following code:
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<i:InvokeCommandAction Command="{Binding OnLoadedCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
And then simply implemented a DelegateCommand called OnLoadedCommand in the View's respective ViewModel. Inside that command I call my desired method.
Please comment if you spot anything wrong with this approach! I chose to try this since it required the least amount of changes to my code, but I may be missing some vital information regarding problems the solution may cause.
I've been at this for weeks...I am creating a WPF application that uses Avalon Dock 2.0 in the the Main Window. I am trying to use the Docking Manager in a MVVM way, so I have DockingManager.DocumentsSource bound to an ObservableCollection<object> property in my MainViewModel. I also created a custom DataTemplateSelector and bound it to DockingManager.LayoutItemTemplateSelector. The problem I am having:
I add a ViewModel to the documents source.
My custom DataTemplateSelector.SelectTemplate() is called.
The item parameter in SelectTemplate() is a System.Windows.Controls.ContentPresenter instead of the ViewModel object that I added.
Even if I return the correct DataTemplate, it ends up getting bound to the ContentPresenter instead of the ViewModel contained within the ContentPresenter.
I managed to replicate the problem in a bare-bones WPF project, here is the relevant code:
MainWindow:
<!-- MainWindow markup DataContext is bound to
I omitted the usual xmlns declarations -->
<Window
xmlns:xcad="http://schemas.xceed.com/wpf/xaml/avalondock"
xmlns:local="clr-namespace:AvalonTest"
Title="MainWindow">
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Grid>
<xcad:DockingManager DocumentsSource="{Binding Docs}">
<xcad:DockingManager.LayoutItemTemplateSelector>
<local:TestTemplateSelector>
<local:TestTemplateSelector.TheTemplate>
<DataTemplate>
<local:TestView/>
</DataTemplate>
</local:TestTemplateSelector.TheTemplate>
</local:TestTemplateSelector>
</xcad:DockingManager.LayoutItemTemplateSelector>
<xcad:LayoutRoot>
<xcad:LayoutPanel Orientation="Vertical">
<xcad:LayoutAnchorablePane/>
<xcad:LayoutDocumentPane/>
</xcad:LayoutPanel>
</xcad:LayoutRoot>
</xcad:DockingManager>
</Grid>
</Window>
MainViewModel:
class MainViewModel
{
//Bound to DockingManager.DocumentsSource
public ObservableCollection<object> Docs { get; private set; }
public MainViewModel()
{
Docs = new ObservableCollection<object>();
Docs.Add(new TestViewModel());
}
}
DataTemplateSelector:
class TestTemplateSelector : DataTemplateSelector
{
public TestTemplateSelector() {}
public DataTemplate TheTemplate { get; set; }
//When this method is called, item is always a ContentPresenter
//ContentPresenter.Content will contain the ViewModel I add
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
//Just return the only template no matter what
return TheTemplate;
}
}
TestView:
<!-- TestTemplateSelector will always return this TestView -->
<UserControl x:Class="AvalonTest.TestView"
xmlns:local="clr-namespace:AvalonTest">
<Grid>
<StackPanel Orientation="Vertical">
<TextBox Text="{Binding TestText}"/>
<Button Content="A Button"/>
</StackPanel>
</Grid>
</UserControl>
TestViewModel:
//TestView.DataContext should be set to this, but instead
//it gets set to a containing ContentPresenter
class TestViewModel : ObservableObject
{
private string testText = "TESTTESTTEST";
public string TestText
{
get { return testText; }
set
{
testText = value;
RaisePropertyChanged("TestText");
}
}
}
The Result:
TestView is not properly bound to the TestViewModel and therefore "TESTTESTTEST" does not show up in the TextBox. I have checked out Avalon Dock's sample MVVM project and their DataTemplateSelector always gets the ViewModel instead of ContentPresenter. What am I doing wrong?
Change the definition for SelectTemplate on TestTemplateSelector as follows:
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
//check if the item is an instance of TestViewModel
if (item is TestViewModel)
return TheTemplate;
//delegate the call to base class
return base.SelectTemplate(item, container);
}
You should always check if the item passed is an instance of your target view model and if isn't, delegate the call to the base class so WPF can handle the objects you don't care about.