Declaring TabItems in XAML, binding SelectedItem to view model - c#

I'd like to bind the SelectedItem of a TabControl to a corresponding field in my view model, however while still declaring the available items within the TabControl itself (as opposed to using ItemsSource) and retrieving their Content as for the actual SelectedItem. Meaning: If a tab is being selected, it's Content should end up as the SelectedItem (not the TabItem) and vice-versa.
Sample view model, inheriting a Caliburn.Micro conductor without (!) collection:
public class MyViewModelConductor : Conductor<ConductedViewModelBase> {
public ViewModelA { get; set; }
public ViewModelB { get; set; }
}
And the corresponding TabControl in XAML:
<TabControl SelectedItem="{Binding ActiveItem}">
<TabItem cal:View.Model="{Binding ViewModelA}">
<TabItem.Header> <!-- vm specific fancy UI stuff --> </TabItem.Header>
</TabItem>
<TabItem cal:View.Model="{Binding ViewModelB}">
<TabItem.Header> <!-- vm specific fancy UI stuff --> </TabItem.Header>
</TabItem>
</TabControl>
I'm aware I could just use e.g. Conductor<'1>.Collection.OneActive and bind to ItemsSource, but there are a few reasons I'd like to refrain from doing so:
The available view models from the conductor's side is a fixed set, declared by the exposed properties, not an infinite collection
For the UI's side, for each TabItem I need to declare a specific header with lots of just UI related stuff (icon, color) which is platform-specific, hence I would not like to leak it into my view models.
I've tried utilizing SelectedValue and SelectedValuePath and binding to the TabControl itself (e.g. SelectedValue="{Binding RelativeSource={RelativeSource Self}, Path=SelectedItem}" SelectedValuePath="Content"), but there WPF tries to look for a Content property on my bound view models as soon as one becomes selected.

You would have to bind the SelectedValue to your view model class. Then use the SelectedValuePath to declare which property of the SelectedItem is used as the SelectedValue.
<TabControl SelectedValuePath="Content"
SelectedValue="{Binding ViewModelSelectedContentProperty}">
<TabItem cal:View.Model="{Binding ViewModelA}">
<TabItem.Header> <!-- vm specific fancy UI stuff --> </TabItem.Header>
</TabItem>
<TabItem cal:View.Model="{Binding ViewModelB}">
<TabItem.Header> <!-- vm specific fancy UI stuff --> </TabItem.Header>
</TabItem>
</TabControl>
But I don't recommend this. You should bind the ItemSource to a set of data models and define DataTemplates to give you a clean design and dynamic/extensible behavior.
Usually you would use DataTemplate for the TabControl.ContentTemplate to template the content and TabControl.ItemTemplate to template the TabItem (TabControl header).
Don't hardcode the ItemsControl items. They're is absolutely no reason to avoid data templating.

Related

WPF TabControl not firing Loaded event of unfocused children

I have a TabControl with a binding for the ItemsSource property. It's bound to a ObservableCollection. When I add an item to the collection, the tabs get created correctly, but only the first tab gets its Grid_Loaded event fired. I'm guessing this is because it's focused. I need to initialize stuff when a new tab opens, it contains a control that needs to be referenced.
<TabControl x:Name="tabSessions"
ItemsSource="{Binding Sessions}"
SelectedIndex="0"
BorderThickness="0"
Padding="0,0,0,0">
<TabControl.ItemTemplate>
<DataTemplate DataType="{x:Type vm:MyViewModel}">
<TextBlock Margin="4,4,16,4" Text="{Binding Name}"></TextBlock>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<Grid Loaded="Grid_Loaded">
<!-- View here -->
</Grid>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
Is there an event that I could use to initialize this? I also need the DataContext to be set, and the view to be initialized.c#
The DataTemplate is reused by the tabs, which is why you're probably only seeing one Loaded event. It's only loaded the first time and then reused when you switch tabs. Only the binded content is changed.
Instead of listening to Loaded on the Grid, you could rather listen to DataContextChanged which will tell you every time the DataContext is set to a new object.
The DataContext will change when you switch tabs. The current tab will be set as the DataContext of the DataTemplate (and by extensions the Grid).
When using a DataTemplate you assume that the view is going to be the same, but the content is going to differ. If this is not the case and the views will differ based on the content, you'll probably want to look into using a DataTemplateSelector. This will let you define several DataTemplates and select one of them to use based on the current DataContext. You can read up on DataTemplateSelector in the Microsoft Docs

c# mvvm bind views to tabcontrol with header

I have a wpf programme with a main View (Window)which contains a TabControl to show several different UserControl Views (the sub-views, one in each tab). Every View has an associated ViewModel.
I wish to bind the TabControl so that I just need to load a new sub-view into the ApplicationViewModel and it will appear on the TabControl.
I have successfully bound the sub-views to the content, but cannot seem to get anything in the header. I wish to bind the header to a property in the sub-view's ViewModel, specifically TabTitle.
Application View (DataTemplate binding not working):
<Window ...>
<DockPanel>
<TabControl ItemsSource="{Binding PageViews}" SelectedIndex="0"> <!--Working-->
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding DataContext.TabTitle}, Path=DataContext.TabTitle}" /> <!--Not Working-->
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
</DockPanel>
</Window>
Application ViewModel (ObservableObject basically implements INotifyPropertyChanged`):
class ApplicationViewModel : ObservableObject
{
private DataManager Data;
private ObservableCollection<UserControl> _pageViews;
internal ApplicationViewModel()
{
Data = new DataManager();
PageViews.Add(new Views.MembersView(new MembersViewModel(Data.DataSet)));
}
public ObservableCollection<UserControl> PageViews
{
get
{
if (_pageViews == null)
{
_pageViews = new ObservableCollection<UserControl>();
}
return _pageViews;
}
}
The MembersView Code behind:
public partial class MembersView : UserControl
{
public MembersView(MembersViewModel ViewModel)
{
InitializeComponent();
DataContext = ViewModel;
}
}
MembersViewModel (truncated):
public class MembersViewModel : INotifyPropertyChanged
{
public TabTitle { get; protected set; }
public MembersViewModel(DataSet BBDataSet)
{
TabTitle = "Members";
}
//All view properties
}
I'm sure that it is something simple...
You are binding the TabControl to a collection of type UserControl. That means the data context for each item will be of type UserControl. There is no property named "TabTitle" in UserControl, so the binding will not work.
I think what you are trying to do can be accomplished with the following changes:
Have ApplicationViewModel expose a collection of type MembersViewModel, instead of UserControl, and populate it appropriately.
Setup a ContentTemplate to create views for your items in the TabControl:
<TabControl.ContentTemplate>
<DataTemplate DataType="{x:Type namespace:MembersViewModel}">
<namespace:MembersView />
</DataTemplate>
</TabControl.ContentTemplate>
(Replace "namespace:" with your xaml imported namespace containing your controls.)
Update the ItemTemplate in your TabControl so it binds properly to the view model:
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding TabTitle}}" />
</DataTemplate>
</TabControl.ItemTemplate>
Update MembersView to have a parameterless constructor. The DataContext on the view will be set for you by the TabControl. If you need to access the view model from your code-behind, it should be available through the DataContext property after the InitializeComponent() call.
Anytime you are working with ItemsControl (and its extensions such as ListBox, TreeView, TabControl, etc.), you should never be instantiating your own item views. You always want to setup a template that instantiates the view based on the data (or view model) and bind directly to the data (or view model) in the ItemsSource property. This allows all of the item's data contexts to be setup for you so you can bind to them.
Edit: Since you have multiple view / viewmodel pairings, you will want to define your templates slightly differently:
<TabControl ItemsSource="{Binding PageViews}" SelectedIndex="0">
<TabControl.Resources>
<DataTemplate DataType="{x:Type namespace:MembersViewModel}">
<namespace:MembersView />
</DataTemplate>
<DataTemplate DataType="{x:Type namespace:ClassesViewModel}">
<namespace:ClassesView />
</DataTemplate>
<DataTemplate DataType="{x:Type namespace:SessionsViewModel}">
<namespace:SessionsView />
</DataTemplate>
</TabControl.Resources>
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding TabTitle}}" />
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
The difference is that you want to define multiple data templates, one for each type, in your resources. That means it will use those templates each time it encounters those types. You still want to set ItemTemplate to force the tab headers to use a specific template. However, do not set ContentTemplate, allowing the content to use the data templates defined in resources.
I hope that makes sense.
P.S. You can also define these data templates in a higher level resource dictionary, such as in your main window or your application, if you want them to apply to content presenters every place you use those view models, rather than only in this one TabControl.

How to set ItemsSource for a databinding from the code-behind

I cannot figure out how to set ItemsSource to my Pivot programatically. I am using MVVM Light ViewModelLocator where my ViewModel is registered. Then I set the DataSource of my Page and in the xaml of the Pivot I set its ItemsSource. But in the ViewModel I have other collection that I want to change at runtime to be an ItemsSource for my Pivot:
Here is my ViewModelLocator:
public class ViewModelLocator
{
public ViewModelLocator()
{
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
SimpleIoc.Default.Register<TripTypeViewModel>();
}
public TripTypeViewModel TripTypeVM
{
get
{
return ServiceLocator.Current.GetInstance<TripTypeViewModel>();
}
}
public static void Cleanup() {}
}
XAML of the page:
<Page
...
DataContext = "{Binding Source={StaticResource Locator}, Path=TripTypeVM }">
...
<Pivot x:Name="TripsSegmentsPivot" Title="Locator" Foreground="#FF888888" Style="{StaticResource PivotStyle1}" SelectionChanged="Pivot_SelectionChanged" Margin="0" Grid.Row="1" ItemTemplate="{StaticResource TripTypeTemplate1}" ItemsSource="{Binding TripTypeViewModelDataSource}">
<Pivot.HeaderTemplate>
<DataTemplate>
<Grid>
<TextBlock Text="{Binding id}"/>
</Grid>
</DataTemplate>
</Pivot.HeaderTemplate>
</Pivot>
So in the ViewModel except TripTypeViewModelDataSource i have also TripTypeViewModelDataSource2.
In the xaml.cs of the View I would usually do this:
TripsSegmentsPivot.ItemsSource = ViewModelLocator.TripTypeVM.TripTypeViewModelDataSource;
TripsSegmentsPivot.ItemsSource = ViewModelLocator.TripTypeVM.TripTypeViewModelDataSource2;
but its not working..
There are many ways to bind but let's discuss a few...
Directly to the Pivot object itself: http://msdn.microsoft.com/en-us/library/system.windows.controls.itemscontrol.itemssource(v=vs.105).aspx
MyPivot.Itemsource=mycollection;
You could use a collection view source: http://msdn.microsoft.com/en-us/library/system.windows.data.collectionviewsource(v=vs.110).aspx Visual Stuidio will create these if you drag a container control onto the surface, it also put code in the code behind so you can "wire-it-up" It works very nicely with any collection type and fully integrates with LINQ.
You can set up observable collection in the View Model and Bind to them from the view. The easiest way to do this is to create a Static Instance of the View Model in the View XAML itself because visual studio property page will "See" the view model and allow you to pick the property as an item source. All you need to do is implement INPC or create a Dependency Property (where needed) and you are all set to go. You need this to notify the view when a property changes. https://www.google.com/webhp?sourceid=chrome-instant&ion=1&espv=2&ie=UTF-8#q=using%20inotifypropertychanged
If you need to change collections at run time, the CollectionViewSource is dead-simple to use.. like this...
MyCollectionViewSource.Source = MyCollection.Where(p=>p.Name==SelectedName).ToList();
MyCollectionViewSource.Source = MyCollection.Where(p=>p.ID > 500).ToList();
MyCollectionViewSource.Source = MyOtherCollection.ToList();
MyCollectionViewSource.Source = JustSayNoToObamaCare();
In the current code it shows:
ItemsSource="{Binding TripTypeViewModelDataSource}"
If you use a CollectionViewSource like this:
ItemsSource="{Binding MyCVS}"
You can then change the collection at will in the viewmodel like this.
MyCVS.Source = Collection1;
MyCVS.Source = Collection2;

Looking for guidance for where to put some code in a WPF MVVM application

First time attempting MVVM, looking for clarity on where to put some code.
My main view will need to bind to a list that will be holding 1 to many UserControls.
Would the List exist in the ViewModel or the Model? From what I'm reading, the model contains properties typically that the View binds to via the ViewModel. I don't see how that would work for this, the Model would not need to know about the List of UserControls which is a list of a View(UserControl), I may be making this harder than needed, but I'm still wrapping my mind around where code should be placed and I want to understand it. Thanks for any guidance, or if I did not explain myself well please let me know.
Your UserControls should have a ViewModel (Let's call it ItemViewModel by now).
Your MainViewModel should have an ObservableCollection<ItemViewModel>.
Then your view should have an ItemsControl (or one of its derivatives such as ListBox) for which the ItemsSource property will be bound to the ObservableCollection.
And then the ItemsControl should define an ItemTemplate that contains your UserControl.
This is the right way to do what you're describing with WPF / MVVM.
Example (pseudocode):
MainViewModel:
public class MainViewModel
{
public ObservableCollection<ItemViewModel> Items {get;set;}
}
MainView:
<Window>
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<my:UserControl/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Window>
Keep in mind that each instance of UserControl will have it's DataContext set to the corresponding item in the source collection.
Therefore, if your ItemsViewModel looks something like this:
public class ItemsViewModel
{
public string LastName {get;set;}
public string FirstName {get;set;}
//INotifyPropertyChanged, etc.
}
your UserControl can be defined like this:
<UserControl>
<StackPanel>
<TextBox Text="{Binding LastName}"/>
<TextBox Text="{Binding FirstName}"/>
</StackPanel>
</UserControl>
You shouldn't need a list of UserControls. What you would likely have is your View binding to a List of items in your ViewModel. For example, create a ListBox and set it's ItemsSource to your ViewModel's list.
To create a your user control for each item, you would need to create a DataTemplate for the type in your list and specify your UserControl and you can give any bindings inside that usercontrol to the item.
The ListBox will then use the DataTemplate to create a UserControl for each item in the list.

MVC design issue

I'm having an application using MVC. It has a canvas and property grid. When an item is selected in the canvas. The property grid should display its details.
So I made an event listener and when item is selected in the canvas it raises an event to the controller which pass the selected item to the property grid to display the details.
Model :
Item object containing name, description
Controller :
protected Controller(object model, FrameworkElement view)
{
this._model = model;
this._view = view;
}
public virtual void Initialize()
{
View.DataContext = Model;
}
View :
<TextBlock>Status</TextBlock>
<ComboBox ItemsSource="?????"/>
Where view is the property grid and model is the selected item.
The problem is in the property grid there is a dropdown list containing lookup values how can I get the dropdown values given that the datacontext of the property grid has already been set to the selected item which doesn't contain reference to these lookup items.
I know that it's easy to use custom code to do that. But I don't want to violate the MVC aproach.
Bind to a source rather than DataContext, sources are provided by ElementName, RelativeSource & Source, so you can name the View for example and use ElementName to get it as source then the Path could be DataContext.LookupValues or whatever your property in the model (- the DataContext of the View is your model -) is called.
e.g.
<Window ...
Name="Window">
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Name}" />
<ComboBox ItemsSource="{Binding ElementName=Window, Path=DataContext.Occupations}"
SelectedItem="{Binding Occupation}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- ... --->
Edit: Your problem seems to be that you do not pass the information you need, consider a design which still grants you access to more than just the SelectedItem of some list, e.g.
<Window ...
Name="Window">
<ListBox Name="listBox" ItemsSource="{Binding Data}" />
<ContentControl DataContext="{Binding ElementName=listBox, Path=SelectedItem}">
<ComboBox ItemsSource="{Binding ElementName=Window, Path=DataContext.Occupations}"
SelectedItem="{Binding Occupation}" />
</ContentControl>
<!-- ... --->
The DataContext of the ContentControl may be the SelectedItem of the ListBox but the ComboBox inside can still reference the DataContext of the Window which should provide the necessary information.
This is similar to my first example in that the DataContext inside the DataTemplate is always an item of the collection but you can access external DataContexts using sources in your bindings.

Categories

Resources