I have two usercontrols inside of a TabItem. The TabItem has it's own ViewModel, which has a property that the TabItem's child ContentControl's content bound to. This property represents another viewmodel, which will change the view depending on which one it is. Here's an example:
<TabItem DataContext="{Binding Path=MainLayerTabViewModel, Source={StaticResource ViewModelLocator}}" Header="Layers">
<ContentControl Content="{Binding ChildViewModel}">
<ContentControl.Resources>
<DataTemplate DataType="{x:Type vm:LayersViewModel}">
<views:LayersTabView DataContext="{Binding ChildViewModel}"/>
</DataTemplate>
<DataTemplate DataType="{x:Type vm:UserDrawnLayersViewModel}">
<views:AlternateLayersTabView DataContext="{Binding ChildViewModel}" />
</DataTemplate>
</ContentControl.Resources>
</ContentControl>
Here's the view model used as the datacontext for the tabitem:
public class MainLayerTabViewModel : ViewModelBase
{
public object ChildViewModel { get; set; }
public MainLayerTabViewModel()
{
ChildViewModel = (App.Current.Resources["ViewModelLocator"] as ViewModelLocator).LayersViewModel;
}
}
Now, the two types of possible ViewModels for the ChildViewModel are LayersViewModel and UserDrawnLayersViewModel. When I change ChildViewModel to one of those, the view is properly switched via the DataTemplate. But the DataContext isn't actually being set. Nothing is being bound. I tried creating separate properties for each ViewModel in the MainLayerTabViewModel and binding the DataContext of each view to its own property, but that didn't work either.
I haven't verified this, however I can see couple of issue with your code.
View should be
<ContentControl Content="{Binding ChildViewModel}">
<ContentControl.Resources>
<DataTemplate DataType="{x:Type vm:LayersViewModel}">
<views:LayersTabView/>
</DataTemplate>
<DataTemplate DataType="{x:Type vm:UserDrawnLayersViewModel}">
<views:AlternateLayersTabView/>
</DataTemplate>
</ContentControl.Resources>
</ContentControl>
ViewModel:
public class MainLayerTabViewModel : ViewModelBase
{
public ViewModelBase ChildViewModel { get; set; }
public MainLayerTabViewModel()
{
ChildViewModel = new LayersViewModel();
//or ChildViewModel = new UserDrawnLayersViewModel();
}
}
Hope that helps..
Related
I can not get the binding of a text property for a DataTemplate in MVVM design pattern.
To show the problem I expose below a simplification of my problem, where I bind two different view properties to the same model property (aka AnObject.Text).
My code in MainWindow.xaml is:
...
<Button Grid.Row="0" Content="{Binding ButtonText}" />
...
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding MyItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<DockPanel>
<Label Content="aaaaa" />
<TextBlock Text="{Binding ItemText}" />
</DockPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
...
My code behind in MainWindow.xaml.cs (which sets the same DataContext for Button and every item in <ItemsControl ItemsSource>):
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowViewModel();
}
My code in MainWindowViewModel.cs is:
...
public ObservableCollection<object> MyItems => MyConverter.GetCollection(MyData.List);
public string ItemText => "dddd"; // this DOES works
public string ItemText => AnObject.Text; // this does NOT work
...
public string ButtonText => AnObject.Text; // this DOES works (note, same object property!)
...
Any idea why my binding inside the DataTemplate does not work?
Thanks in advance!
There are various things to understand here:
Button control will have the DataContext set to MainWindowViewModel instance. This is the reason why ButtonText variable value is getting reflected in Button control text.
For ItemsControl the DataContext is the the same as for the Button, i.e. the MainWindowViewModel instance.
Each item in the ItemsControl ItemsSource acts as a DataContext for the elements in the ItemTemplate, i.e. the DockPanel and its child elements. This is managed automatically by the framework. So essentially you will need a public property named ItemText in the class which will act as a DataContext for Dockpanel.
In your case the ItemText property is not the part of the objects which are in list.
I have a main View and corresponding ViewModel. Because the main View is too complicated so I split it into many small Views; and each small View also has its own ViewModel.
My question is that how to "associating a sub ViewModel to a sub View" in the main View?
I am doing the following way, not sure if it is right or I have to use DataTemplate?
<StackPanel>
<local:SmallView-A DataContext="{x:Type local:SmallViewModel-A}" />
</StackPanel>
<StackPanel>
<local:SmallView-B DataContext="{x:Type local:SmallViewModel-B}" />
</StackPanel>
<StackPanel>
<local:SmallView-C DataContext="{x:Type local:SmallViewModel-C}" />
</StackPanel>
I would saif that the right anwser is the following :-)... (not tested!)
in a generic.xaml, associated views and viewmodels
In your MainViewModel create properties for each sub viewmodel
In your MainView, bind content presenter to there properties
generic
<DataTemplate DataType="{x:Type ViewModels:SubViewModel1}">
<Views:SubView1 />
</DataTemplate>
<DataTemplate DataType="{x:Type ViewModels:SubViewModel2}">
<Views:SubView2 />
</DataTemplate>
MainViewModel
public SubViewModel1 Sub1 { get; set; }
public SubViewModel2 Sub2 { get; set; }
MainView
<Grid>
## your layout here
<ContentPresenter Content="{Binding Sub1}" />
<ContentPresenter Content="{Binding Sub2}" />
##could be
<TabControl ItemSource="{Binding MyViewModelCollection}" />
</Grid>
It turns out to use the following code likely.
<ContentControl Content="{Binding MyViewInstance}">
<ContentControl.Resources>
<DataTemplate DataType="x:Type vm:MyViewModel">
</DataTemplate>
</ContentControl.Resources>
</ContentControl>
In your 'Views' folder(Create one if you have not got one already) create a new user control called `SmallViewA'.
In your main XAML:
xmlns:views = "clr-namespace:[Your app name].Views"
<views:SmallViewA x:Name = "vSmallViewA" Loaded = "SmallViewALoaded"/>
private void SmallViewALoaded(object sender, RoutedEventArgs e)
{
vSmallViewA.DataContext = YoursmallAViewModelshouldgohere;
// Initialise
}
Add your SmallViewA control the the SmallViewA control in the Views folder. From there you can bind your properties from your SmallViewA Viewmodel.
Follow the same procedure above for SmallViewB and SmallViewC
Note: be sure to implement the INotifyPropertyChanged to your ViewModel.
I've got two data templates in the resources and a DataTemplateSelector to choose between them:
<DataGrid.Resources>
<DataTemplate x:Key="RedTemplate">
<TextBlock Text="{Binding **Name1OrName2**}" />
</DataTemplate >
<DataTemplate x:Key="GreenTemplate">
....
</DataTemplate>
<local:MyTemplateSelector x:Key="MyTemplateSelector"
RedTemplate="{StaticResource RedTemplate}"
GreenTemplate="{StaticResource GreenTemplate}" />
</DataGrid.Resources>
Here is the code-behind of the selector:
public class MyTemplateSelector : DataTemplateSelector
{
public DataTemplate RedTemplate { get; set; }
public DataTemplate GreenTemplate { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if (item is RedItem) return RedTemplate;
else if (item is GreenItem) return GreenTemplate;
else return base.SelectTemplate(item, container);
}
}
As long as I use MyTemplateSelector for one DataColumn (say, Name1), it works fine. But my DataGrid has two template columns to be bound to two string fields: Name1 and Name2
<DataGridTemplateColumn CellTemplateSelector="{StaticResource MyTemplateSelector}" > // Should be bound to Name1
<DataGridTemplateColumn CellTemplateSelector="{StaticResource MyTemplateSelector}" > // Should be bound to Name2
My question is: how can I set the proper Path (Name1 or Name2) in the Binding (instead of Name1OrName2, see above). Thank you.
It looks like my original answer was due to a misunderstanding of the question, and the requirement isn't about the data template selector, but rather how to parameterize the property a binding binds to, so you can use the same template for two different properties.
Quick answer: That's not the way XAML is designed to be used. You can't parameterize the Path property of a Binding. The conventional solution is to write one template for each case. It would be nice if you could specify which property/field a DataGridTemplateColumn is meant to display, via a Binding or DisplayMemberPath property, and then passed that value to the template -- but it doesn't work that way.
I found a likely-looking workaround here, but I'm not sure the ROI on it would stack up well relative to copying and pasting a DataTemplate and getting on with your life.
If the templates are complicated enough for maintenance to be a concern, you can work around that like so:
XAML resources:
<DataTemplate x:Key="RedBaseTemplate">
<Border BorderBrush="Green" BorderThickness="2" Margin="1">
<Label x:Name="Text" Background="Red" Content="{Binding}" />
</Border>
</DataTemplate>
<DataTemplate x:Key="GreenBaseTemplate">
<Border BorderBrush="Red" BorderThickness="2" Margin="1">
<Label x:Name="Text" Background="Green" Content="{Binding}" />
</Border>
</DataTemplate>
<DataTemplate x:Key="RedTemplateA">
<ContentControl
Content="{Binding A}"
ContentTemplate="{StaticResource RedBaseTemplate}"
/>
</DataTemplate>
<DataTemplate x:Key="RedTemplateB">
<ContentControl
Content="{Binding B}"
ContentTemplate="{StaticResource RedBaseTemplate}"
/>
</DataTemplate>
<DataTemplate x:Key="GreenTemplateA">
<ContentControl
Content="{Binding A}"
ContentTemplate="{StaticResource GreenBaseTemplate}"
/>
</DataTemplate>
<DataTemplate x:Key="GreenTemplateB">
<ContentControl
Content="{Binding B}"
ContentTemplate="{StaticResource GreenBaseTemplate}"
/>
</DataTemplate>
Original Answer
This is a common pattern: You want multiple instances of the same DataTemplateSelector (or value converter, quite often), but with different parameters. The solution is to derive from MarkupExtension, so you can instantiate the thing at the point of use with its own unique parameters, rather than creating one shared instance someplace else as a resource. In this case DataTemplateSelector is a class rather than an interface, so you can't derive your selector from MarkupExtension. Instead you write a quick MarkupExtension that returns your selector.
I wanted to pass the templates themselves to RedGreenTemplateSelectorExtension using StaticResource or DynamicResource in the XAML, but the XAML parser didn't like the idea. But this works well enough.
public class RedGreenTemplateSelectorExtension : MarkupExtension
{
public Object RedTemplateKey { get; set; }
public Object GreenTemplateKey { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider)
{
var redTemplate = new StaticResourceExtension(RedTemplateKey)
.ProvideValue(serviceProvider) as DataTemplate;
var greenTemplate = new StaticResourceExtension(GreenTemplateKey)
.ProvideValue(serviceProvider) as DataTemplate;
return new RedGreenTemplateSelector() {
RedTemplate = redTemplate,
GreenTemplate = greenTemplate
};
}
}
public class RedGreenTemplateSelector : DataTemplateSelector
{
public DataTemplate RedTemplate { get; set; }
public DataTemplate GreenTemplate { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if (item is RedItem)
return RedTemplate;
else if (item is GreenItem)
return GreenTemplate;
else
return base.SelectTemplate(item, container);
}
}
XAML
<StackPanel>
<ContentControl
ContentTemplateSelector="{local:RedGreenTemplateSelector RedTemplateKey=RedTemplate, GreenTemplateKey=GreenTemplate}"
>
<local:RedItem/>
</ContentControl>
<ContentControl
ContentTemplateSelector="{local:RedGreenTemplateSelector RedTemplateKey=RedTemplate, GreenTemplateKey=GreenTemplate}"
>
<local:GreenItem/>
</ContentControl>
</StackPanel>
P.S. StaticResource and Binding are two very different classes that do very different things. People misuse "binding" to mean "assignment". It's not. You aren't using any bindings at all here.
I am farily new to mvvm so bear with me. I have 2 View models which are inherited namely DBViewModel and PersonViewModel. i would like to add the person object in DBViewModel and bind 2 combobox with observablecollection in PersonViewModel.
public class PersonViewModel
{
private ICommand AddCommand ;
public Person PersonI{get;set;}
public ObservableCollection<Person> EmployeeList{ get; set; }
public ObservableCollection<String> OccupationList{ get; set; }
public PersonViewModel()
{
PersonI = new Person();
this.AddCommand = new DelegateCommand(this.Add);
// get OccupationList and EmployeeList
}
......
}
public class DBViewModel : PersonViewModel
{
public PersonViewModel PersonVM { get; set; }
public PersonViewModel()
{
PersonVM = new PersonViewModel();
}
....
}
<DataTemplate DataType='{x:Type viewModel:DBViewModel}'>
<StackPanel>
<TextBox Text="{Binding PersonI.Name}" />
<ComboBox Name="cboccupation" ItemsSource="{Binding OccupationList}"
DisplayMemberPath="Name"
SelectedItem="{Binding SelectedItem}" SelectedValuePath="Id"/>
<Button Content="Add" Command="{Binding AddCommand}" />
<DataGrid ItemsSource="{Binding EmployeeList}" CanUserAddRows="True">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Occupation">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding OccupationList}"
DisplayMemberPath="Name" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
</DataTemplate>
if you want an easier approach I'm thinking you could set a datastore field using blend and bind both controls to that field.
Your bindings are trying to bind to the properties PersonI and OccupationList on the DBViewModel, however those properties do not exist.
You need to point them to the PersonVM.PersonI and PersonVM.OccupationList instead.
<TextBox Text="{Binding PersonVM.PersonI.Name}" />
<ComboBox ItemsSource="{Binding PersonVM.OccupationList}" ... />
For your ComboBox binding inside the DataGrid, that probably will not work because the DataContext of each row in the Grid is a Person object (specified by the DataGrid.ItemsSource), and I don't think Person has a property called OccupationList.
You need to change the Source of your binding to use the object that has the OccupationList property.
For example, if your DataGrid was named MyDataGrid, the following binding for that ComboBox would work
<ComboBox ItemsSource="{Binding
ElementName=MyDataGrid,
Path=DataContext.PersonVM.OccupationList}" ... />
Alternatively, you could use a RelativeSource binding to have it look for the parent DataGrid object without needing to specify a name
<ComboBox ItemsSource="{Binding
RelativeSource={RelativeSource AncestorType={x:Type DataGrid}},
Path=DataContext.PersonVM.OccupationList}" ... />
As a side note, you seem to be a bit confused about bindings and the DataContext. I like to blog about beginner WPF topics, and would recommend reading What is this "DataContext" you speak of?. I find it has helped many WPF beginners on this site understand the basic binding concept. :)
We're using Caliburn.Micro/Silverlight 4 and life is good.
I'm trying to bind a combobox's itemsSource to a viewModel, but this doesn't seem possible since the combobox is already bound to its own row's dataItem. The logic which fills the combo changes with other data on the screen so I can't really use a static list like I've been using.
Is there a way to bind directory to the viewModel somehow??? I've tried element to element binding but this never appears to work within the grid.
<Controls:DataGridTemplateColumn x:Name="FooNameCol" Header="Foo" MinWidth="200">
<Controls:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Path=Foo.ShortName}"
Style="{StaticResource DataGridTextColumnStyle}"/>
</StackPanel>
</DataTemplate>
</Controls:DataGridTemplateColumn.CellTemplate>
<Controls:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox DisplayMemberPath="ShortName"
MinWidth="200" MinHeight="25"
SelectedItem="{Binding Path=Officer, Mode=TwoWay, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
ItemsSource="{Binding Officers, Source={StaticResource ReferenceListRetriever}}" />
</DataTemplate>
</Controls:DataGridTemplateColumn.CellEditingTemplate>
</Controls:DataGridTemplateColumn>
Within a DataTemplate, the DataContext is bound to each single item of the corresponding list; since all Bindings implicitly refers to DataContext, you have to ensure that the path is valid, starting from the single data item.
In your scenario, for the indicated binding to work, you should have a VM shaped this way:
public class MyVM {
public IEnumerable<MyItem> Items {get;}
}
public class MyItem {
public Foo Foo {get;}
public Officer Officer {get;set;}
public IEnumerable<Officer> Officers {get;}
}
It may seem an overkill, but in some scenarios each combo can actually contain different choices for each data item, based on some business rule.
In simpler cases MyItem can just expose a common list coming from the parent MyVM:
public class MyItem {
...
public IEnumerable<Officer> Officers {
get { return _parent.AvailableOfficers; }
}
}
If you really can't live with it and prefer to keep the available Officers list in the root VM only, you can use a Xaml side trick:
public class MyVM {
public IEnumerable<MyItem> Items {get;}
public IEnumerable<Officer> Officers {get;}
}
public class MyItem {
public Foo Foo {get;}
public Officer Officer {get;set;}
}
Xaml:
<UserControl ...>
...
<AnyFrameworkElementAtThisLevel Name="bridge" />
...
<Controls:WhateverGrid>
...
<Controls:DataGridTemplateColumn ...>
<Controls:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
...
</DataTemplate>
</Controls:DataGridTemplateColumn.CellTemplate>
<Controls:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox DisplayMemberPath="ShortName"
SelectedItem="{Binding Path=Officer, Mode=TwoWay, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
ItemsSource="{Binding DataContext.Officers, ElementName=bridge}" />
</DataTemplate>