Bind ItemsControl to a collection of ViewModels on a ViewModel - c#

This is my first time posting a question. I've simplified my code as much as possible to illustrate what I'm looking for.
I have a ViewModel (outer) that contains an ObservableCollection of another ViewModel (inner). The inner ViewModel is for a UserControl. The outer ViewModel is for MainWindow. I simply want to display one UserControl for each item in the ObservableCollection. But, I'm having trouble getting the UserControl's DataContext set to the items in the ObservableCollection.
Inner ViewModel (for UserControl):
public class InnerViewModel : ViewModelBase
{
string _text;
public string Text
{
get { return _text; }
set { SetProperty<string>(ref _text, value); }
}
public InnerViewModel() { }
}
Inner ViewModel (for UserControl):
public class OuterViewModel : ViewModelBase
{
ObservableCollection<InnerViewModel> _innerViewModels;
public ObservableCollection<InnerViewModel> InnerViewModels
{
get { return _innerViewModels; }
set { SetProperty<ObservableCollection<InnerViewModel>>(ref _innerViewModels, value); }
}
public OuterViewModel()
{
_innerViewModels = new ObservableCollection<InnerViewModel>();
}
public void Init()
{
InnerViewModels.Clear();
InnerViewModels.Add(new InnerViewModel { Text = "Item1" });
InnerViewModels.Add(new InnerViewModel { Text = "Item2" });
}
}
InnerControl XAML (outermost tag removed for cleanliness)
<UserControl.DataContext>
<local:InnerViewModel />
</UserControl.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50px"></ColumnDefinition>
<ColumnDefinition ></ColumnDefinition>
<ColumnDefinition Width="50px"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Label Content="Header"></Label>
<Label Grid.Column="1" Content="{Binding Text}" ></Label>
<Label Grid.Column="2" Content="Footer"></Label>
</Grid>
MainWindow XAML
<Window.DataContext>
<local:OuterViewModel />
</Window.DataContext>
<Grid>
<ItemsControl ItemsSource="{Binding InnerViewModels}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:InnerControl></local:InnerControl> <!-- HOW DO I SET THE DATACONTEXT ON THIS??? -->
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
InnerControl.cs Code:
public partial class InnerControl : UserControl
{
public InnerControl()
{
InitializeComponent();
}
}
MainWindow.cs Code:
public partial class MainWindow : Window
{
OuterViewModel _vm;
public MainWindow()
{
InitializeComponent();
_vm = (OuterViewModel)DataContext;
_vm.Init();
}
}
ViewModelBase:
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] String propertyName = null)
{
if (Equals(storage, value))
{
return false;
}
storage = value;
this.OnPropertyChanged(propertyName);
return true;
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler eventHandler = this.PropertyChanged;
if (eventHandler != null)
{
eventHandler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Result:
Screenshot of what I get when I run

I solved this as follows:
Changed MainWindow.cs to create the outer view model:
public partial class MainWindow : Window
{
OuterViewModel _vm;
public MainWindow()
{
InitializeComponent();
_vm = new OuterViewModel();
_vm.Init();
DataContext = _vm;
}
}
Change MainWindow to NOT have DataContext set
<!-- Don't set DataContext here -->
<Grid>
<ItemsControl ItemsSource="{Binding InnerViewModels}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:InnerViewModel}">
<local:InnerControl DataContext="{Binding}"></local:InnerControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
Changed InnerControl XAML to NOT have DataContext set:
<!-- Don't set DataContext here -->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50px"></ColumnDefinition>
<ColumnDefinition ></ColumnDefinition>
<ColumnDefinition Width="50px"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Label Content="Header"></Label>
<Label Grid.Column="1" Content="{Binding Text}" ></Label>
<Label Grid.Column="2" Content="Footer"></Label>
</Grid>

In you view for the inner VM you create the the view-model in the view (view-first), that means your view that you create in the DataTemplate has a different view-model than the one supplied by the ItemsControl.
You could maybe overwrite that again like this (not sure about the property assignment order):
<DataTemplate>
<local:InnerControl DataContext="{Binding}"/>
</DataTemplate>
As noted in the comment, i would not create the VMs in the view, but create the views implicitly using typed DataTemplates.

Related

Navigate to another tab item by a button click from the tab having multiple view models doesn't work

I have to navigate to another tab by a button click from the first tab in a WPF MVVM Application(c#).
I am trying to achieve this by adding binding to Selected Index property in tab control.There are two different view models are used in the first tab.After adding binding to Selected Index property in tab control it loses the rest of view model's access and No data is present on the text boxes in the first tab. Also navigation is not working . how can I use tab navigation if the window has multiple view models. please see sample code.
XAML file
MainWindow.xaml
<Grid>
<TabControl SelectedIndex="{Binding SelectedTab,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
DataContext="{Binding processVM}">
<TabItem Header="Home">
<Grid ShowGridLines="false" >
<Grid.ColumnDefinitions >
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="50" />
<RowDefinition Height="50" />
<RowDefinition Height="50" />
</Grid.RowDefinitions>
<TextBox Name="txtCustomerName"
Grid.Row="0" Grid.Column="1"
Text="{Binding CustomerName}"
DataContext="{Binding customerVM}"></TextBox>
<TextBox Name="txtDepositAmount"
Grid.Row="1" Grid.Column="1"
Text="{Binding DepositAmount}"
DataContext="{Binding customerVM}"></TextBox>
<Button Content="Click" Width="100" Height="50"
Grid.Row="2"
DataContext="{Binding processVM}"
Command="{Binding ButtonCommand}"
/>
</Grid>
Code behind
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowViewModel()
{
processVM = new ProcessViewModel(),
customerVM = new CustomerViewModel()
};
}
}
View Models
MainWindowViewModel.cs
class MainWindowViewModel
{
public ProcessViewModel processVM { get; set; }
public CustomerViewModel customerVM { get; set; }
}
ProcessViewModel.cs
public class ProcessViewModel: INotifyPropertyChanged
{
private string depositAmount;
public string DepositAmount
{
get { return depositAmount; }
set {
depositAmount = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("DepositAmount"));
}
}
public event PropertyChangedEventHandler PropertyChanged;
private ICommand m_ButtonCommand;
public ICommand ButtonCommand
{
get
{
return m_ButtonCommand;
}
set
{
m_ButtonCommand = value;
}
}
private int selectedTab;
public int SelectedTab
{
get { return selectedTab; }
set
{
selectedTab = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("SelectedTab"));
}
}
public ProcessViewModel()
{
ButtonCommand = new RelayCommand(new Action<object>(clickbutton));
depositAmount = "450";
}
public void clickbutton(object obj)
{
MessageBox.Show("clicked");
SelectedTab = 1;
}
}
CustomerViewModel.cs
class CustomerViewModel: ProcessViewModel, INotifyPropertyChanged
{
private string customerName;
public string CustomerName
{
get { return customerName; }
set { customerName = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("CustomerName"));
}
}
public CustomerViewModel()
{
CustomerName = "Alex";
}
public event PropertyChangedEventHandler PropertyChanged;
}
Before adding binding for selected index there were no issues.
Your problem is that you're setting TabControl.DataContext. DataContext is inherited, so all the controls inside it are now using processVM as their binding source instead of MainWindowViewModel.
Instead of setting TabControl.DataContext, change the SelectedIndex binding to this:
SelectedIndex="{Binding processVM.SelectedTab, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"

C#: Binding textbox to object attribute

I am new to object binding and I don' succeed to make it work.
I have a xaml window with the following textbox:
<Grid x:Name="gr_main" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="180,65,0,0" DataContext="{Binding currentproj}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox Grid.Row="0" Grid.Column="2" x:Name="txt_localdir" Height="25" TextWrapping="Wrap" Width="247" IsEnabled="False" Text="{Binding Path=Localdir, UpdateSourceTrigger=PropertyChanged}"/>
In the cs code of the main window, I define an instance of my Project class, called currentproj, as follows:
public partial class MainWindow : Window{
Project currentproj;
public MainWindow()
{
currentproj = new Project();
InitializeComponent();
}}
The project class (defined in a Project.cs file) is as follows:
public partial class Project : Component, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
private string _localdir;
public string Localdir
{
get { return _localdir; }
set
{
if (value != _localdir)
{
_localdir = value;
NotifyPropertyChanged("Localdir");
}
}
}
public Project()
{
InitializeComponent();
}
public Project(IContainer container)
{
container.Add(this);
InitializeComponent();
}}
However, even if I am binding the textbox.text attribute to the Localdir path of the currentproj object, the textbox is never updated. I see the PropertyChanged event is alwais null when I set the value of Localdir, but I don't understand why.
Data binding works on the DataContext. The Grid's DataContext is not set correctly, this should be removed.
so the Grid definition should be:
<Grid x:Name="gr_main" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="180,65,0,0">
Setting the Window DataContext to currentProj is done by:
public partial class MainWindow : Window{
Project currentproj;
public MainWindow()
{
currentproj = new Project();
DataContext = currentproj;
InitializeComponent();
}}

How to get ComboBox content value?

I would like to get content from my combobox. I already tried some ways to do that, but It doesn't work correctly.
This is example of my combobox:
<ComboBox x:Name="cmbSomething" Grid.Column="1" Grid.Row="5" HorizontalAlignment="Center" Margin="0 100 0 0" PlaceholderText="NothingToShow">
<ComboBoxItem>First item</ComboBoxItem>
<ComboBoxItem>Second item</ComboBoxItem>
</ComboBox>
After I click the button, I want to display combobox selected item value.
string selectedcmb= cmbSomething.Items[cmbSomething.SelectedIndex].ToString();
await new Windows.UI.Popups.MessageDialog(selectedcmb, "Result").ShowAsync();
Why this code does not work?
My result instead of showing combobox content, it shows this text:
Windows.UI.Xaml.Controls.ComboBoxItem
You need the Content property of ComboBoxItem. So this should be what you want:
var comboBoxItem = cmbSomething.Items[cmbSomething.SelectedIndex] as ComboBoxItem;
if (comboBoxItem != null)
{
string selectedcmb = comboBoxItem.Content.ToString();
}
I have expanded on my suggestion regarding using models instead of direct UI code-behind access. These are the required parts:
BaseViewModel.cs
I use this in a lot of the view models in my work project. You could technically implement it directly in a view model, but I like it being centralized for re-use.
public abstract class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private Hashtable values = new Hashtable();
protected void SetValue(string name, object value)
{
this.values[name] = value;
OnPropertyChanged(name);
}
protected object GetValue(string name)
{
return this.values[name];
}
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
ComboViewModel.cs
This what you'll bind to make it easy to get values. I called it ComboViewModel because I'm only dealing with your ComboBox. You'll want a much bigger view model with a better name to handle all of your data binding.
public class ComboViewModel : BaseViewModel
{
public ComboViewModel()
{
Index = -1;
Value = string.Empty;
Items = null;
}
public int Index
{
get { return (int)GetValue("Index"); }
set { SetValue("Index", value); }
}
public string Value
{
get { return (string)GetValue("Value"); }
set { SetValue("Value", value); }
}
public List<string> Items
{
get { return (List<string>)GetValue("Items"); }
set { SetValue("Items",value); }
}
}
Window1.xaml
This is just something I made up to demonstrate/test it. Notice the various bindings.
<Window x:Class="SO37147147.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ComboBox x:Name="cmbSomething" Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="0" HorizontalAlignment="Center" MinWidth="80"
ItemsSource="{Binding Path=Items}" SelectedIndex="{Binding Path=Index}" SelectedValue="{Binding Path=Value}"></ComboBox>
<TextBox x:Name="selectedItem" MinWidth="80" Grid.Row="2" Grid.Column="0" Text="{Binding Path=Value}" />
<Button x:Name="displaySelected" MinWidth="40" Grid.Row="2" Grid.Column="1" Content="Display" Click="displaySelected_Click" />
</Grid>
</Window>
Window1.xaml.cs
Here's the code-behind. Not much to it! Everything is accessed through the dataContext instance. There's no need to know control names, etc.
public partial class Window1 : Window
{
ComboViewModel dataContext = new ComboViewModel();
public Window1()
{
InitializeComponent();
dataContext.Items=new List<string>(new string[]{"First Item","Second Item"});
this.DataContext = dataContext;
}
private void displaySelected_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show(String.Format("Selected item:\n\nIndex: {0}\nValue: {1}", dataContext.Index, dataContext.Value));
}
}
You can add business logic for populating models from a database, saving changes to a database, etc. When you alter the properties of the view model, the UI will automatically be updated.

How to set dependency property of ItemsControl.ItemTemplate and handle its events

I have a main control (MainWindow.xaml) and an user control (ItemView.xaml). MainWindow contains an ItemsControl for all the ItemView-s and a simple button to add an item. All logic is (should be?) inside two corresponding viewmodels (MainWindowViewModel and ItemViewModel). Below is my code (made it as short as possible), but I have two problems with it:
When a new item is added it is correctly displayed but the exception is raised (Cannot create default converter to perform 'two-way' conversions between types 'WpfApplication1.ItemView' and 'WpfApplication1.ItemViewModel'.).
The OnDelete event handler in MainWindowViewModel is never raised? Edit: actually the ViewModel property inside BtnDeleteClick is null so yeah... of course.
Btw - I use Fody PropertyChanged.
MainWindow.xaml:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wpfApplication1="clr-namespace:WpfApplication1"
Title="MainWindow" Height="350" Width="525"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<Button Grid.Row="0" Width="100" Height="35" Content="Add" HorizontalAlignment="Left" Margin="10" Click="BtnAddClick"></Button>
<Border Grid.Row="1" MinHeight="50">
<ItemsControl ItemsSource="{Binding ViewModel.Items}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<wpfApplication1:ItemView ViewModel="{Binding ., PresentationTraceSources.TraceLevel=High, Mode=TwoWay}"></wpfApplication1:ItemView>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
</Grid>
</Window>
MainWindow.xaml.cs:
[ImplementPropertyChanged]
public partial class MainWindow
{
public MainWindowViewModel ViewModel { get; set; }
public MainWindow()
{
InitializeComponent();
ViewModel = new MainWindowViewModel();
}
private void BtnAddClick(object sender, RoutedEventArgs e)
{
ViewModel.Add();
}
}
MainWindowViewModel.cs:
[ImplementPropertyChanged]
public class MainWindowViewModel
{
public ObservableCollection<ItemViewModel> Items { get; set; }
public MainWindowViewModel()
{
Items = new ObservableCollection<ItemViewModel>();
}
public void Add()
{
var item = new ItemViewModel();
item.OnDelete += (sender, args) =>
{
Debug.WriteLine("-- WAITING FOR THIS TO HAPPEN --");
Items.Remove(item);
};
Items.Add(item);
}
}
ItemViewModel.xaml:
<UserControl x:Class="WpfApplication1.ItemView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Grid>
<Button Width="100" Height="35" Content="Delete" Click="BtnDeleteClick"></Button>
</Grid>
</UserControl>
ItemView.xaml.cs:
[ImplementPropertyChanged]
public partial class ItemView
{
public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register
(
"ViewModel", typeof(ItemViewModel), typeof(ItemView), new UIPropertyMetadata(null)
);
public ItemViewModel ViewModel
{
get { return (ItemViewModel)GetValue(ViewModelProperty); }
set { SetValue(ViewModelProperty, value); }
}
public ItemView()
{
InitializeComponent();
}
private void BtnDeleteClick(object sender, RoutedEventArgs e)
{
ViewModel.Delete();
}
}
And ItemViewModel.cs:
[ImplementPropertyChanged]
public class ItemViewModel
{
public event EventHandler OnDelete;
public void Delete()
{
var handler = OnDelete;
if (handler != null)
{
handler(this, new EventArgs());
}
}
}
You should not set
DataContext="{Binding RelativeSource={RelativeSource Self}}"
in the XAML of your ItemView. It effectively breaks the ViewModel="{Binding .}" binding in MainWindow.xaml, because the DataContext is no longer an ItemsViewModel, but an ItemsView.
As a rule, you should never explicitly set the DataContext of a UserControl, because all "external" bindings would then require an explicit Source or RelativeSource value.
That said, you're doing all this way too complicated. Instead of having a button click handler in your ItemsView, you could simply have a view model with a delete command, and bind the Button's Command property to this command.
It may look like this:
public class ItemViewModel
{
public string Name { get; set; }
public ICommand Delete { get; set; }
}
public class MainViewModel
{
public MainViewModel()
{
Items = new ObservableCollection<ItemViewModel>();
}
public ObservableCollection<ItemViewModel> Items { get; private set; }
public void AddItem(string name)
{
Items.Add(new ItemViewModel
{
Name = name,
Delete = new DelegateCommand(p => Items.Remove(p as ItemViewModel))
});
}
}
and would be used like this:
<UserControl x:Class="WpfApplication1.ItemView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<Button Content="Delete"
Command="{Binding Delete}"
CommandParameter="{Binding}"/>
</Grid>
</UserControl>

Communicating with ViewModel from MainView

I am new to MVVM and still trying to get a grasp on it so let me know if I'm setting this up wrong. What I have is a UserControl with a ListView in it. I populate this ListView with data from the ViewModel then add the control to my MainView. On my MainView I have a button that I want to use to add an item to the ListView. Here is what I have:
Model
public class Item
{
public string Name { get; set; }
public Item(string name)
{
Name = name;
}
}
ViewModel
public class ViewModel : INotifyPropertyChanged
{
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
private ObservableCollection<Item> _itemCollection;
public ViewModel()
{
ItemCollection = new ObservableCollection<Item>()
{
new Item("One"),
new Item("Two"),
new Item("Three"),
new Item("Four"),
new Item("Five"),
new Item("Six"),
new Item("Seven")
};
}
public ObservableCollection<Item> ItemCollection
{
get
{
return _itemCollection;
}
set
{
_itemCollection = value;
OnPropertyChanged("ItemCollection");
}
}
}
View (XAML)
<UserControl.Resources>
<DataTemplate x:Key="ItemTemplate">
<StackPanel Orientation="Vertical">
<Label Content="{Binding Name}" />
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<UserControl.DataContext>
<local:ViewModel />
</UserControl.DataContext>
<Grid>
<ListView ItemTemplate="{StaticResource ItemTemplate}" ItemsSource="{Binding ItemCollection}">
</ListView>
</Grid>
MainWindow
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.mainContentControl.Content = new ListControl();
}
private void Button_Add(object sender, RoutedEventArgs e)
{
}
}
MainWindow (XAML)
<Grid>
<DockPanel>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
<Button Width="100" Height="30" Content="Add" Click="Button_Add" />
</StackPanel>
<ContentControl x:Name="mainContentControl" />
</DockPanel>
</Grid>
Now, from what I understand, I should be able to just an item to ItemCollection and it will be updated in the view. How do I do this from the Button_Add event?
Again, if I'm doing this all wrong let me know and point me in the right direction. Thanks
You should not interact directly with the controls.
What you need to do is define a Command (a class that implements the ICommand-interface) and define this command on your ViewModel.
Then you bind the Button's command property to this property of the ViewModel. In the ViewModel you can then execute the command and add an item directly to your list (and thus the listview will get updated through the automatic databinding).
This link should provide more information:
http://msdn.microsoft.com/en-us/library/gg405484(v=pandp.40).aspx#sec11

Categories

Resources