In a WPF project I have a ComboBox to select from different objects. Using an ItemsControl and it's ItemTemplateSelector I am trying to show different UI for the ComboBox selection based on a property of the object. So, in the example below we pick from person objects. In the ItemTemplateSelector we pick a different DataTemplate based on the Person's IsManager property. The trouble is it doesn't work.
I have a suspicion it might be due to the ItemsSource of the ItemsControl being bound to one item, but I am not sure. If this is the problem, what changes can I make to the code to achieve this?
XAML :
<Window x:Class="ItemsSelector.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:local="clr-namespace:ItemsSelector"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.Resources>
<local:Selector x:Key="selector"/>
<DataTemplate x:Key="managerTemplate">
<TextBlock Text="Manager"/>
</DataTemplate>
<DataTemplate x:Key="juniorTemplate">
<TextBlock Text="Junior"/>
</DataTemplate>
</Grid.Resources>
<ComboBox x:Name="cbo" Margin="2" Grid.Row="0" ItemsSource="{Binding .}" SelectedIndex="0">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<ItemsControl Grid.Row="1" ItemTemplateSelector="{StaticResource selector}" ItemsSource="{Binding ElementName=cbo ,Path=SelectedItem}">
</ItemsControl>
</Grid>
CODE BEHIND:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new Person[] {
new Person() { Name = "Boss", IsManager = true },
new Person() { Name = "Underling", IsManager = false }
};
}
}
PERSON:
public class Person
{
public string Name { get; set; }
public bool IsManager { get; set; }
public string Title { get; set; }
}
SELECTOR:
public class Selector : DataTemplateSelector
{
public override DataTemplate
SelectTemplate(object item, DependencyObject container)
{
FrameworkElement element = container as FrameworkElement;
if (element != null && item != null && item is Person)
{
var person = item as Person;
switch (person.IsManager)
{
case true:
return element.FindResource("managerTemplate") as DataTemplate;
case false:
return element.FindResource("juniorTemplate") as DataTemplate;
default:
break;
}
}
return null;
}
}
The ItemsSource property of an ItemsControl can only be bound to a collection that returns an IEnumerable.
You should use a ContentControl to be able to bind display the selected item of the ComboBox:
<ContentControl Grid.Row="1" ContentTemplateSelector="{StaticResource selector}"
Content="{Binding ElementName=cbo ,Path=SelectedItem}">
</ContentControl>
I think I've found the solution. I need to be using ContentTemplateSelector.
Related
I was working on a project when I came across an issue with RaisePropertyChanged from MVVM Light that I can't seem to figure out. When I try to raise a change for my list, the list does get updated, but as does the selected index value above. The value that is passed to my selected index appears to be influenced by what key was pressed to trigger the event (i.e. if I press "BACKSPACE", the value passed to the setter is "-1", whereas if I enter a letter, the value passed is "0")
I recreated a project which purely demonstrates the issue. Below is the main bit of logic found in the MainVeiwModel:
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
_testItems = new List<TestItem>()
{
new TestItem() { Name = "Test1" },
new TestItem() { Name = "Test2" }
};
}
public int SelectedIndex
{
get { return _selectedIndex; }
set
{
_selectedIndex = value;
RaisePropertyChanged("SelectedIndex");
RaisePropertyChanged("SelectedText");
RaisePropertyChanged("TestList");
}
}
public string SelectedText
{
get
{
return _testItems[_selectedIndex].Name;
}
set
{
_testItems[_selectedIndex].Name = value;
RaisePropertyChanged("TextList");
}
}
public List<string> TextList
{
get
{
_textList = new List<string>();
if (_testItems != null && _testItems.Count > 0)
{
foreach (TestItem item in _testItems)
_textList.Add(item.Name);
}
return _textList;
}
set { _textList = value; }
}
private int _selectedIndex;
private List<string> _textList;
private List<TestItem> _testItems;
}
My XAML:
<Window x:Class="RaisePropertyBug.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:local="clr-namespace:RaisePropertyBug"
mc:Ignorable="d"
DataContext="{Binding Source={StaticResource Locator}, Path=Main}"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ComboBox ItemsSource="{Binding TextList, UpdateSourceTrigger=PropertyChanged}"
SelectedIndex="{Binding SelectedIndex, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
<TextBox Grid.Row="1" Text="{Binding SelectedText, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
For context: I have a ComboBox which lists the names from a collection of items I have. There is an edit window where users can change names and other properties of these items. My goal is to have the ComboBox list update as the user edits the value. In my actual program, you are able to do this with the item at index 0, but any other index will automatically change to 0 as soon as a key is pressed and the RaisePropertyChanged() area is reached.
Check below code if it's working as per your requirement.
Use SelectedItem property of ComboBox and bind selecteditem to the edit screen/textbox. I've bind SelectedTestItem.Name propety here.
View -
<Window x:Class="StackOverflow.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:local="clr-namespace:StackOverflow"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ComboBox ItemsSource="{Binding TestItems, UpdateSourceTrigger=PropertyChanged}" DisplayMemberPath="Name" SelectedValuePath="Name"
SelectedItem="{Binding SelectedTestItem, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
<TextBox Grid.Row="1" Text="{Binding SelectedTestItem.Name, UpdateSourceTrigger=PropertyChanged}" Width="200"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
View.cs -
public partial class MainWindow : Window, INotifyPropertyChanged
{
private TestItem selectedTestItem;
public TestItem SelectedTestItem
{
get { return selectedTestItem; }
set
{
selectedTestItem = value;
RaisePropertyChanged("SelectedTestItem");
}
}
public List<TestItem> TestItems
{
get;
set;
}
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
var items = new List<TestItem>()
{
new TestItem() { Name = "Test1" },
new TestItem() { Name = "Test2" }
};
TestItems = items;
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string prop)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(prop));
}
}
}
You don't even need INotifyPropertyChanged for this example. I'm not entirely certain of what you are trying to achieve, but this code will achieve what I have gleaned from your post.
<Window x:Class="RaisePropertyChangedExample.BindingExample"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Correct View" Width="150" Height="80">
<StackPanel Orientation="Vertical">
<ComboBox ItemsSource="{Binding Items}"
x:Name="ItemViews"
HorizontalAlignment="Stretch" VerticalAlignment="Center" DisplayMemberPath="Name"/>
<TextBox DataContext="{Binding SelectedItem, ElementName=ItemViews}" Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Stretch" VerticalAlignment="Center"/>
</StackPanel>
</Window>
and the supporting code
using System.Windows;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace RaisePropertyChangedExample
{
public partial class BindingExample : Window
{
public BindingExample()
{
InitializeComponent();
DataContext = new BindingExampleViewModel();
}
}
public class BindingExampleViewModel
{
public ObservableCollection<TestItemViewModel> Items { get; set; }
= new ObservableCollection<TestItemViewModel>(new List<TestItemViewModel>
{
new TestItemViewModel {Name = "Test1"},
new TestItemViewModel {Name = "Test2"}
});
}
public class TestItemViewModel
{
public string Name { get; set; }
}
}
Unless there is some need of the index of the selected Item, there is no real argument against simply exposing each item as a TestItemViewModel view model and binding the other controls directly to the selected item itself. If however other controls are bound to the members of the TestItemViewModel then it's still not necessarily true that you should implement INotifyPropertyChanged on that view model.
The following example will still display the correct information when wired up with the existing ViewModel:
<Window x:Class="RaisePropertyChangedExample.BindingExample"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Correct View" Width="150" Height="100">
<StackPanel Orientation="Vertical">
<ComboBox ItemsSource="{Binding Items}"
x:Name="Combo"
HorizontalAlignment="Stretch" VerticalAlignment="Center" DisplayMemberPath="Name"/>
<Grid DataContext="{Binding SelectedItem, ElementName=Combo}">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBox Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Stretch" VerticalAlignment="Center"/>
<Label Grid.Row="1" HorizontalAlignment="Stretch" Height="20" Content="{Binding Name}" />
</Grid>
</StackPanel>
</Window>
Normally
Updating after every keystroke can diminish performance and it denies the user the usual opportunity to backspace and fix typing errors before committing to the new value. see MS reference
however, this is only an issue if other processing occurs as the result of updating the source. If you are worried about the amount of processing involved you can switch to the default behaviour of LostFocus by simply omitting `UpdateSourceTrigger' declaration.
I have a problem with bindings for DataTemplate based on defined DataType in ItemsControl, when I want to bind to my custom user control.
For demonstration purposes, I've created simple Item class example, where I have collection of items like this:
public class Item
{
public string ItemNameToBeSureWhatPropertyIsBound { get; set; }
}
In my ViewModel I create such collection, and expose it (with one item for comparison separately):
public class MainWindowViewModel : INotifyPropertyChanged
{
private ObservableCollection<Item> _items;
private Item _exampleItem;
public MainWindowViewModel()
{
Items = new ObservableCollection<Item>(new[] { new Item { ItemNameToBeSureWhatPropertyIsBound = "Me" }, new Item { ItemNameToBeSureWhatPropertyIsBound = "MySelf" }, new Item { ItemNameToBeSureWhatPropertyIsBound = "Ich" }, });
ExampleItem = Items.LastOrDefault();
}
public ObservableCollection<Item> Items
{
get { return _items; }
set { _items = value; OnPropertyChanged(); }
}
public Item ExampleItem
{
get { return _exampleItem; }
set { _exampleItem = value; OnPropertyChanged();}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
My custom user control is defined like this:
<UserControl x:Class="WpfDataTemplate.ItemRowUserControl"
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"
d:DesignHeight="40" d:DesignWidth="300"
x:Name="ItemRowControl" DataContext="{Binding Mode=OneWay, RelativeSource={RelativeSource Self}}">
<Grid Background="Yellow" Height="40">
<TextBlock Text="{Binding ItemName}" Foreground="Black"/>
</Grid>
</UserControl>
...and it has one DependencyProperty in code behind:
public partial class ItemRowUserControl : UserControl
{
public ItemRowUserControl()
{
InitializeComponent();
}
public static readonly DependencyProperty ItemNameProperty = DependencyProperty.Register(
"ItemName", typeof (string), typeof (ItemRowUserControl), new PropertyMetadata(default(string)));
public string ItemName
{
get { return (string) GetValue(ItemNameProperty); }
set { SetValue(ItemNameProperty, value); }
}
}
The problem is, when I try to bind to property of Item in DataTemplate for ItemsControl, which I'm doing in MainWindow like this (note: I have dummy converter for debugging purposes only, returning value back, and nothing more):
<Window.DataContext>
<my:MainWindowViewModel />
</Window.DataContext>
<Window.Resources>
<my:MyDummyConverter x:Key="MyDummyConverter" />
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="50" />
</Grid.RowDefinitions>
<ItemsControl Name="ItemsControl" ItemsSource="{Binding Items}" Grid.Row="0" Background="Red">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type my:Item}">
<my:ItemRowUserControl ItemName="{Binding ItemNameToBeSureWhatPropertyIsBound, Converter={StaticResource MyDummyConverter}}" />
<!--<Grid Background="Pink">
<TextBlock Text="{Binding ItemNameToBeSureWhatPropertyIsBound, Converter={StaticResource MyDummyConverter}}" Foreground="Black" Height="30" />
</Grid>-->
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Grid Grid.Row="1">
<my:ItemRowUserControl ItemName="{Binding DataContext.ExampleItem.ItemNameToBeSureWhatPropertyIsBound, ElementName=MyWindow, Converter={StaticResource MyDummyConverter}}" />
</Grid>
</Grid>
Now, in case I bind to my custom ItemRowUserControl, the value I get into converter (and I see the same in Debug Output) is ItemRowUserControl itself. But if I bind to commented out code, everything works fine. Why is that, and how can I have custom control for DataTemplate so that bindings (offered by intellisense) will work? On the side note: binding to my ItemRowUserControl in grid row 1 (at the bottom) works fine, so I guess control is set to work as expected?
The problem is that you explicitly set the DataContext of your UserControl to itself:
DataContext="{Binding Mode=OneWay, RelativeSource={RelativeSource Self}}
Remove that assignment and write the ItemName binding like this:
<TextBlock Text="{Binding ItemName,
RelativeSource={RelativeSource AncestorType=UserControl}}"/>
or like this
<TextBlock Text="{Binding ItemName, ElementName=ItemRowControl}"/>
I need to bind a List to a UniformGrid into a WP Window using WPF MVVM.
I had in mind to do something like this:
Into my VM:
private List<Rat> rats;
private UniformGrid uniformGrid;
public List<Rat> Rats
{
get { return rats; }
set
{
if (rats != value)
{
//update local list value
rats = value;
//create View UniformGrid
if (uniformGrid == null)
uniformGrid = new UniformGrid() { Rows=10};
else
uniformGrid.Children.Clear();
foreach(Rat rat in value)
{
StackPanel stackPanel = new StackPanel();
Ellipse ellipse = new Ellipse(){Height=20, Width=20, Stroke= Brushes.Black};
if (rat.Sex== SexEnum.Female)
ellipse.Fill= Brushes.Pink;
else
ellipse.Fill= Brushes.Blue;
stackPanel.Children.Add(ellipse );
TextBlock textBlock = new TextBlock();
textBlock.Text= rat.Name + " (" + rat.Age +")";
stackPanel.Children.Add( textBlock );
uniformGrid.Children.Add(stackPanel);
}
OnPropertyChanged("Rats");
}
}
}
The VM is correctly informed when the list needs to be refreshed into the view via an event.
So at this point I would need my View to be correctly bound to the VM. I made it this way:
<GroupBox x:Name="GB_Rats" Content="{Binding Rats}" Header="Rats" HorizontalAlignment="Left" Height="194" Margin="29,10,0,0" VerticalAlignment="Top" Width="303">
Is this the correct global approch?
Concretely, when attempting to run the code, this line fails to execute:
uniformGrid = new UniformGrid() { Rows=10};
->
An unhandled exception of type 'System.InvalidOperationException' occurred in PresentationCore.dll
Additional information: The calling thread must be STA, because many UI components require this.
This lets me think that I should not proceed this way from a MVVM point of view.
Thx for your kind assistance.
The ViewModel isn't supposed to instantiate any UI controls, this should be the View's responsibility.
So in your code you shouldn't try to create StackPanels, Ellipses etc.
Also try to use the types that already have Change notification - for instead of
List<T> use ObservableCollection<T> MSDN, i wouldn't recommend replacing a whole list when its value change.
The right way to do this in the MVVM pattern is to create a DataTemplate for the Rat like this:
ViewModel:
public class MainWindowViewModel
{
public ObservableCollection<Rat> Rats { get; set; } =
new ObservableCollection<Rat>()
{
new Rat()
{
Name = "Fred",
Age = "19",
Sex = SexEnum.Male
},
new Rat()
{
Name = "Martha",
Age = "21",
Sex = SexEnum.Female
}
};
}
Model - Rat and Sex:
public class Rat
{
public SexEnum Sex { get; set; }
public string Name { get; set; }
public string Age { get; set; }
}
public enum SexEnum
{
Female,
Male
}
As you want to present the Models value of Sex in one of two colors you should use a IValueConverter for that:
[ValueConversion(typeof(SexEnum), typeof(Brush))]
public class SexToColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (!(value is SexEnum))
throw new ArgumentException("value not of type StateValue");
SexEnum sv = (SexEnum)value;
//sanity checks
if (sv == SexEnum.Female)
return Brushes.Red;
return Brushes.Blue;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
This is then used in your window:
Window:
<Window x:Class="WpfApplication1.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:local="clr-namespace:WpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525"
xmlns:ViewModel="WpfApplication1.VM"
xmlns:Converters ="clr-namespace:WpfApplication1.Converters"
>
<Grid>
<Grid.Resources>
<Converters:SexToColorConverter x:Key="SexToBrushConverter"></Converters:SexToColorConverter>
</Grid.Resources>
<ComboBox x:Name="comboBox" ItemsSource="{Binding Rats}" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="120">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Ellipse Width="10" Height="10" Fill="{Binding Sex, Converter={StaticResource SexToBrushConverter}}"></Ellipse>
<TextBlock Margin="5" Text="{Binding Name}"></TextBlock>
<TextBlock Margin="5" Text="{Binding Age}"></TextBlock>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</Window>
Note the DataTemplate that is assigned to the ComboBox.ItemTemplate property and the declaration of the Converters:SexToColorConverter and its usage to change the color of the ellipse in the Fill binding.
Update 4.4.2016 16:30
Window using a GroupBox with CheckBoxes to display the list
<Window x:Class="WpfApplication1.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:local="clr-namespace:WpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525"
xmlns:Converters ="clr-namespace:WpfApplication1.Converters"
xmlns:vm="clr-namespace:WpfApplication1.VM">
<Window.DataContext>
<vm:MainWindowViewModel/>
</Window.DataContext>
<Grid>
<Grid.Resources>
<Converters:SexToColorConverter x:Key="SexToBrushConverter"></Converters:SexToColorConverter>
</Grid.Resources>
<GroupBox>
<ItemsControl ItemsSource="{Binding Rats}" >
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox Margin="5"></CheckBox>
<Ellipse Width="10" Height="10" Fill="{Binding Sex, Converter={StaticResource SexToBrushConverter}}"></Ellipse>
<TextBlock Margin="5" Text="{Binding Name}"></TextBlock>
<TextBlock Margin="5" Text="{Binding Age}"></TextBlock>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</GroupBox>
</Grid>
</Window>
I guess the target is to also select the Rats, depending on how MVVM purist you want to be you'd add a List of RatViewModels, that have a bool IsChecked property and bind the ItemsSource to a ObservableCollection<RatViewModel> and synchronize this list with your Models List<Rat>
How to switch user controls based on treeview Selection Change. I have acheived this on ListBox but couldn't figure out how to do that with Wpf Treeview. Here is my XAML Code.
<Window x:Class="MainScreen"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:viewModelSettings="clr-namespace:ViewModel.Settings" >
<Window.Resources>
<DataTemplate DataType="{x:Type viewModelSettings:BasicSettingsViewModel}">
<viewSettings:BasicSettingsView/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewModelSettings:AdvancedSettingsViewModel}">
<viewSettings:AdvancedSettingsView/>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="3*"/>
</Grid.ColumnDefinitions>
<ListBox x:Name="ListBoxMenu"
Grid.Column="0" Margin="5,5,5,385"
ItemsSource="{Binding Settings}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" Padding="10"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Border Grid.Column="1" Margin="5">
<ContentControl Content="{Binding ElementName=ListBoxMenu, Path=SelectedItem}"/>
</Border>
</Grid>
</Window>
I am using Data Template to define Various Viewmodels and binded views with them
To Make it completely MVVM, Here is my code behind
public partial class MainScreen : Window
{
public MainScreen()
{
InitializeComponent();
DataContext = new OptionsDialogViewModel();
}
}
// OptionsDialogViewModel Class
public class OptionsDialogViewModel : ViewModelBase
{
private readonly ObservableCollection<SettingsViewModelBase> _settings;
public ObservableCollection<SettingsViewModelBase> Settings
{
get { return this._settings; }
}
public OptionsDialogViewModel ()
{
_settings = new ObservableCollection<SettingsViewModelBase>();
_settings.Add(new BasicSettingsViewModel());
_settings.Add(new AdvancedSettingsViewModel());
}
}
// SettingsViewModelBase class
public abstract class SettingsViewModelBase : ViewModelBase
{
public abstract string Name { get; }
}
and now my ViewModel(s) are derived from this SettingsViewModelBase
public class AdvancedSettingsViewModel : SettingsViewModelBase
{
public override string Name
{
get { return "Advanced"; }
}
}
I have 2 questions now, Is this the right approach to do this task ?
How can I switch my list view to treeview
Unfortunately selecting items with a treeview is not as straight-forward as selecting with a ListBox.
Unlike simply binding to a ListBox with Content="{Binding ElementName=ListBoxMenu, Path=SelectedItem}", binding to a treeview item involves a bit of code-behind. Check out SO threads here and here for more discussion on the how's and why's.
I'm following this How to create tab-able content in WPF/C#? but I want each tab to show a datagrid. The datagrid doesn't show and also doesn't show the data. When I step into the code, I do see 0,1 being set.
MainWindow.xaml
<Window x:Class="MVVMDataInstances.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:viewModel="clr-namespace:MVVMDataInstances"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<viewModel:MainWindowViewModel />
</Window.DataContext>
<Grid>
<Grid.Resources>
<DataTemplate x:Key="ContentTemplate"
DataType="{x:Type viewModel:ChildViewModel}"/>
</Grid.Resources>
<TabControl ContentTemplate="{StaticResource ContentTemplate}"
ItemsSource="{Binding Items}" />
</Grid>
</Window>
MainWindowViewModel.cs
public ObservableCollection<ChildViewModel> Items { get; private set; }
public MainWindowViewModel()
{
Items = new ObservableCollection<ChildViewModel> {new ChildViewModel(0), new ChildViewModel(1)};
}
ChildView.xaml
<UserControl x:Class="MVVMDataInstances.View"
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:ViewModel="clr-namespace:MVVMDataInstances"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.DataContext>
<ViewModel:ChildViewModel/>
</UserControl.DataContext>
<DataGrid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" ItemsSource="{Binding Grid, Mode=TwoWay}" />
</UserControl>
ChildViewModel.cs
public class ChildViewModel : ViewModelBase
{
private ObservableCollection<ChildModel> _grid;
public ObservableCollection<ChildModel> Grid
{
get { return _grid; }
private set
{
_grid = value;
OnPropertyChanged("Grid");
}
}
public ChildModel Data { get; set; }
public ChildViewModel()
{
}
public ChildViewModel(int tabNumber)
{
Data = new ChildModel {A = tabNumber.ToString(CultureInfo.InvariantCulture)};
Grid = new ObservableCollection<ChildModel> {Data};
}
}
ChildModel.cs
public class ChildModel
{
public string A { get; set; }
public string B { get; set; }
}
ViewModelBase.cs
public class ViewModelBase : INotifyPropertyChanged
{
public void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
I would like to see one grid per tab. The entry on the first tab has a value of 0 for property A. The entry of the second tab has a value of 1 for the property B.
I see that when OnPropertyChanged is called PropertyChanged is null.
I can access the datagrid if I have this in MainWindow.xaml
<Grid>
<TabControl ItemsSource="{Binding Items}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding TabTitle}"/>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<DataGrid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" AutoGenerateColumns="True"
ItemsSource="{Binding Grid}"/>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
But OnPropertyChanged is always null for this and I don't see the grid
<DataTemplate x:Key="ContentTemplate"
DataType="{x:Type viewModel:ChildViewModel}">
<viewModel:ChildView />
</DataTemplate>
Your DataTemplate is empty. From your question, you probably want to do something along these lines:
<Grid>
<Grid.Resources>
<DataTemplate x:Key="ContentTemplate" TargetType="{x:Type viewModel:ChildViewModel}">
<DataGrid AutoGenerateColumns="False" . . .>
<DataGrid.Columns>
<!-- Your column definitions here -->
</DataGrid.Columns>
</DataGrid>
</DataTemplate>
</Grid.Resources>
<TabControl ContentTemplate="{StaticResource ContentTemplate}"
ItemsSource="{Binding Items}" />
</Grid>
This DataTemplate tells the ChildViewModel.cs that it's appearance is View.xaml :
<DataTemplate x:Key="ContentTemplate"
DataType="{x:Type viewModel:ChildViewModel}">
<viewModel:View />
</DataTemplate>
As a result behind the scenes it also sets each View's DataContext to an instance of ChildViewModel.
I follow this MVVM: ViewModel inheritance to have ViewModel Inheritance. The reason PropertyChanged was null because the constructor was initialized twice. First time with an integer, and then second time with the InitializedComponent. If I comment out the InitializedComponent, the grid was never initialized. The answer to the other stackoverflow question makes everything cleaner with view model inheritance.