I've just stuck in a problem to bind collection in ItemsControl with ItemTeplate that contains bounded ComboBox.
In my scenario I need to "generate" form that includes textbox and combobox for each item in collection and let user to update items. I could use DataGrid for that but I'd like to see all rows in edit mode, so I use ItemsControl with custom ItemTemplate.
It's ok to edit textboxes but when you try to change any ComboBox, all other ComboBoxes in other rows will change too.
Is it a bug or feature?
Thanks, Ondrej
Window.xaml
<Window x:Class="ComboInItemsControlSample.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="480" Width="640">
<Window.Resources>
<CollectionViewSource x:Key="cvsComboSource"
Source="{Binding Path=AvailableItemTypes}" />
<DataTemplate x:Key="ItemTemplate">
<Border BorderBrush="Black" BorderThickness="0.5" Margin="2">
<Grid Margin="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="20" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Text="{Binding Path=ItemValue}" />
<ComboBox Grid.Column="2"
SelectedValue="{Binding Path=ItemType}"
ItemsSource="{Binding Source={StaticResource cvsComboSource}}"
DisplayMemberPath="Name"
SelectedValuePath="Value" />
</Grid>
</Border>
</DataTemplate>
</Window.Resources>
<Grid>
<ItemsControl ItemsSource="{Binding Path=SampleItems}"
ItemTemplate="{StaticResource ItemTemplate}"
Margin="10" />
</Grid>
Window.xaml.cs
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
DataContext = new ViewModel();
}
}
public class ViewModel
{
public ViewModel()
{
SampleItems = new List<SampleItem> {
new SampleItem { ItemValue = "Value 1" },
new SampleItem { ItemValue = "Value 2" },
new SampleItem { ItemValue = "Value 3" }
};
AvailableItemTypes = new List<SampleItemType> {
new SampleItemType { Name = "Type 1", Value = 1 },
new SampleItemType { Name = "Type 2", Value = 2 },
new SampleItemType { Name = "Type 3", Value = 3 },
new SampleItemType { Name = "Type 4", Value = 4 }
};
}
public IList<SampleItem> SampleItems { get; private set; }
public IList<SampleItemType> AvailableItemTypes { get; private set; }
}
public class SampleItem : ObservableObject
{
private string _itemValue;
private int _itemType;
public string ItemValue
{
get { return _itemValue; }
set { _itemValue = value; RaisePropertyChanged("ItemValue"); }
}
public int ItemType
{
get { return _itemType; }
set { _itemType = value; RaisePropertyChanged("ItemType"); }
}
}
public class SampleItemType : ObservableObject
{
private string _name;
private int _value;
public string Name
{
get { return _name; }
set { _name = value; RaisePropertyChanged("Name"); }
}
public int Value
{
get { return _value; }
set { _value = value; RaisePropertyChanged("Value"); }
}
}
public abstract class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName) {
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
Picture
here you can see the result on picture
I believe it's because you're binding to a CollectionViewSource, which tracks the current item. Try binding directly to your list instead, which won't track the current item
<ComboBox Grid.Column="2"
SelectedValue="{Binding Path=ItemType}"
DisplayMemberPath="Name"
SelectedValuePath="Value"
ItemsSource="{Binding RelativeSource={
RelativeSource AncestorType={x:Type ItemsControl}},
Path=DataContext.AvailableItemTypes}" />
While you have a combobox in each row, it doesnt see these comboboxes as being seperate. i.e. They are all using the same collection, and the same selectedValue, so when a value changes in one box, it changes in all of them.
The best way to fix this is to add the SampleItemType collection as a property on your SampleItem model and to then bind the combo box to that property.
Related
I have a listview that can be filtered using a textbox:
<TextBox TextChanged="txtFilter_TextChanged" Name="FilterLv"/>
In the view code-behind I do the following:
CollectionView view = (CollectionView)CollectionViewSource.GetDefaultView(this.lv.ItemsSource);
view.Filter = UserFilter;
private bool UserFilter(object item)
{
if (String.IsNullOrEmpty(FilterLv.Text))
return true;
else
{
DataModel m = (item as DataModel);
bool result = (m.Name.IndexOf(Filter.Text, StringComparison.OrdinalIgnoreCase) >= 0 ||
//m.Surname.IndexOf(Filter.Text, StringComparison.OrdinalIgnoreCase) >= 0);
return result;
}
}
private void Filter_TextChanged(object sender, TextChangedEventArgs e)
{
CollectionViewSource.GetDefaultView(this.lv.ItemsSource).Refresh();
}
Now I have placed a label in the view and I would like this label to show the number of items currently displayed in the listview.
How can I do it? I have found things like this but I don't understand at all what is RowViewModelsCollectionView. In this link it is suggested to bind as below:
<Label Content="{Binding ModelView.RowViewModelsCollectionView.Count}"/>
Could anyone explain me or provide a very little and simple example on how to do it?
FINAL UPDATE:
View model:
public class TestViewModel
{
// lv is populated later in code
public ObservableCollection<DataModel> lv = new ObservableCollection<DataModel>();
public ObservableCollection<DataModel> LV
{
get
{
return this.lv;
}
private set
{
this.lv= value;
OnPropertyChanged("LV");
}
}
private CollectionView view;
public TestViewModel()
{
this.view = (CollectionView)CollectionViewSource.GetDefaultView(this.LV);
view.Filter = UserFilter;
}
private string textFilter;
public string TextFilter
{
get
{
return this.textFilter;
}
set
{
this.textFilter= value;
OnPropertyChanged("TextFilter");
if (String.IsNullOrEmpty(value))
this.view.Filter = null;
else
this.view.Filter = UserFilter;
}
}
private bool UserFilter(object item)
{
if (String.IsNullOrEmpty(this.TextFilter))
return true;
else
{
DataModel m = (item as DataModel);
bool result = (m.Name.IndexOf(this.TextFilter, StringComparison.OrdinalIgnoreCase) >= 0 ||
//m.Surname.IndexOf(this.TextFilter, StringComparison.OrdinalIgnoreCase) >= 0);
return result;
}
}
/// <summary>
/// Número de registros en la listview.
/// </summary>
public int NumberOfRecords
{
get
{
return this.view.Count;
}
}
}
View (xaml):
<!-- search textbox - filter -->
<TextBox TextChanged="txtFilter_TextChanged"
Text="{Binding TextFilter, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}">
<!-- label to show the number of records -->
<Label Content="{Binding NumberOfRecords}"/>
view code-behind (xaml.cs):
private void txtFilter_TextChanged(object sender, TextChangedEventArgs e)
{
CollectionViewSource.GetDefaultView((DataContext as TestViewModel).LV).Refresh();
}
It is filtering ok when I type in the search textbox and listview is updated correctly but the number of records is always 0.
What am i doing wrong?
ATTEMPT2:
Below another attempt not working. If I attach my listivew to the View declared in model view then no items are shown. If I attach listview to LV in model view then items are shown, and when I filter through my search textbox it filters ok, listview is updated but the number of rows shown in the listview always remains to 0.
Notes:
I am using NET 3.5 Visual Studio 2008.
I need to set View as writable in model view because I do not set it
in view model constructor, instead i set it in LoadData method in
view model. LoadData is called from view code-behind constructor.
View Model:
namespace MyTest.Example
{
public Class TestViewModel : INotifyPropertyChanged // Implementations not here to simplify the code here.
{
private ObservableCollection<DataModel> lv;
public ObservableCollection<DataModel> LV
{
get
{
return this.lv;
}
private set
{
this.lv = value;
OnPropertyChanged("LV");
}
}
public CollectionView View { get; set; }
public TestViewModel()
{
this.LV = new ObservableCollection<DataModel>();
// this.View = (CollectionView)CollectionViewSource.GetDefaultView(this.LV);
// this.View.Filter = UserFilter;
}
private string textFilter = string.Empty;
public string TextFilter
{
get
{
return this.textFilter ;
}
set
{
this.textFilter = value;
OnPropertyChanged("TextFilter");
this.View.Refresh();
}
}
private bool UserFilter(object item)
{
if (String.IsNullOrEmpty(this.TextFilter))
return true;
else
{
DataModel m = (item as DataModel);
bool result = (m.Name.IndexOf(this.TextFilter, StringComparison.OrdinalIgnoreCase) >= 0 ||
//m.Surname.IndexOf(this.TextFilter, StringComparison.OrdinalIgnoreCase) >= 0);
return result;
}
}
public void LoadData()
{
this.LV = LoadDataFromDB();
this.View = (CollectionView)CollectionViewSource.GetDefaultView(this.LV);
this.View.Filter = UserFilter;
}
} // End Class
} // End namespace
View code-behing (xaml.cs):
namespace MyTest.Example
{
public Class TestView
{
public TestView()
{
InitializeComponent();
(DataContext as TestViewModel).LoadData();
}
}
}
View (xaml):
xmlns:vm="clr-namespace:MyTest.Example"
<!-- search textbox - filter -->
<TextBox Text="{Binding Path=TextFilter, UpdateSourceTrigger=PropertyChanged}">
<!-- label to show the number of records -->
<Label Content="{Binding Path=View.Count}" ContentStringFormat="No. Results: {0}"/>
<ListView Grid.Row="1" Grid.Column="0" ItemsSource="{Binding Path=View}" SelectionMode="Extended" AlternationCount="2">
ATTEMPT 3:
Finally I have get it to work. Solution is the same as ATTEMPT2 but making below changes:
I have replaced this:
public CollectionView View { get; set; }
by this one:
private CollectionView view;
public CollectionView View {
get
{
return this.view;
}
private set
{
if (this.view == value)
{
return;
}
this.view = value;
OnPropertyChanged("View");
}
}
All the rest remains the same as in ATTEMPT2. In view View.Count and assigning View as ItemsSource to my listview now is working all perfectly.
You should use
<Label Content="{Binding ModelView.Count}"/>
instead of
<Label Content="{Binding ModelView.RowViewModelsCollectionView.Count}"/>
RowViewModelsCollectionView in the other question is the same as ModelView is in your case.
Edit
Count is a property from the CollectionView
For further information have a look at the MSDN
Edit 2
When you dont want to do it via XAML like in my example you have to implement INotifyPropertyChanged and raise this whenever the bound property is changed because otherwiese the UI won't get the change.
In your case: you have to call OnPropertyChanged("NumberOfRecords"); in your filter method. But it would be easier to do it via xaml like i Wrote earlier.
Here is a fully working example with the CollectionView in the view model, and the filter count automatically flowing to the bound control. It uses my mvvm library for the base ViewModel class to supply INotifyPropertyChanged, but you should easily be able to substitute your own system, I'm not doing anything special with it.
The full source code can be downloaded from here
XAML:
<Window
x:Class="FilterWithBindableCount.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:local="clr-namespace:FilterWithBindableCount"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="MainWindow"
Width="525"
Height="350"
d:DataContext="{d:DesignInstance local:MainWindowVm}"
mc:Ignorable="d">
<Grid Margin="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Label
Grid.Row="0"
Grid.Column="0"
Margin="4">
Filter:
</Label>
<TextBox
Grid.Row="0"
Grid.Column="1"
Margin="4"
VerticalAlignment="Center"
Text="{Binding Path=FilterText, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2"
Margin="4"
Text="{Binding Path=PeopleView.Count, StringFormat={}Count: {0}}" />
<DataGrid
Grid.Row="3"
Grid.Column="0"
Grid.ColumnSpan="2"
Margin="4"
CanUserAddRows="False"
CanUserSortColumns="True"
ItemsSource="{Binding Path=PeopleView}" />
</Grid>
</Window>
View models:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Data;
using AgentOctal.WpfLib;
namespace FilterWithBindableCount
{
class MainWindowVm : ViewModel
{
public MainWindowVm()
{
People = new ObservableCollection<PersonVm>();
PeopleView = (CollectionView) CollectionViewSource.GetDefaultView(People);
PeopleView.Filter = obj =>
{
var person = (PersonVm)obj;
return person.FirstName.ToUpper().Contains(FilterText.ToUpper() ) || person.LastName.ToUpper().Contains(FilterText.ToUpper());
};
People.Add(new PersonVm() { FirstName = "Bradley", LastName = "Uffner" });
People.Add(new PersonVm() { FirstName = "Fred", LastName = "Flintstone" });
People.Add(new PersonVm() { FirstName = "Arnold", LastName = "Rimmer" });
People.Add(new PersonVm() { FirstName = "Jean-Luc", LastName = "Picard" });
People.Add(new PersonVm() { FirstName = "Poppa", LastName = "Smurf" });
}
public ObservableCollection<PersonVm> People { get; }
public CollectionView PeopleView { get; }
private string _filterText = "";
public string FilterText
{
get => _filterText;
set
{
if (SetValue(ref _filterText, value))
{
PeopleView.Refresh();
}
}
}
}
class PersonVm:ViewModel
{
private string _firstName;
public string FirstName
{
get {return _firstName;}
set {SetValue(ref _firstName, value);}
}
private string _lastName;
public string LastName
{
get {return _lastName;}
set {SetValue(ref _lastName, value);}
}
}
}
This is actually significantly easier when properly following MVVM. The CollectionView is either declared in the XAML, or as a property in the viewmodel. This allows you to bind directly to CollectionView.Count.
Here is an example of how to place the CollectionViewSource in XAML from one of my apps:
<UserControl
x:Class="ChronoPall.App.TimeEntryList.TimeEntryListView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:app="clr-namespace:ChronoPall.App"
xmlns:componentModel="clr-namespace:System.ComponentModel;assembly=WindowsBase"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:ChronoPall.App.TimeEntryList"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DataContext="{d:DesignInstance local:TimeEntryListViewVm}"
d:DesignHeight="300"
d:DesignWidth="300"
mc:Ignorable="d">
<UserControl.Resources>
<CollectionViewSource x:Key="TimeEntriesSource" Source="{Binding Path=TimeEntries}">
<CollectionViewSource.SortDescriptions>
<componentModel:SortDescription Direction="Descending" PropertyName="StartTime.Date" />
<componentModel:SortDescription Direction="Ascending" PropertyName="StartTime" />
</CollectionViewSource.SortDescriptions>
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="EntryDate" />
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
</UserControl.Resources>
<Grid IsSharedSizeScope="True">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding Source={StaticResource TimeEntriesSource}}">
<ItemsControl.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate DataType="{x:Type CollectionViewGroup}">
<local:TimeEntryListDayGroup />
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ItemsControl.GroupStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:TimeEntryListItem />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</UserControl>
It doesn't actually bind to Count, but it could easily do that with:
<TextBlock Text="{Binding Path=Count, Source={StaticResource TimeEntriesSource}}/>
To do it in the viewmodel, you would just create a readonly property of ICollectionView, and set it equal to CollectionViewSource.GetDefaultView(SomeObservableCollection), then bind to that.
"EDIT"
What I want to achieve is something like -
MainWindow.xaml contains a Button, a ComboBox and a ContentControl.
UserControl-A and UserControl-C both contains a ContentControl.
UserControl-B and UserControl-D both contains a TextBlock.
UserControl-B will be Content of ContentControl Contained in
UserControl-A. UserControl-D will be Content of ContentControl
Contained in UserControl-C.
UserControl-A Or UserControl-C will be Content of ContentControl
Contained in MainWindow.xaml on click of the Button.
On change in SelectedValue of Combobox Update TextBlock of
UserControl-B Or UserControl-D.
"EDIT"
In addition to MainWindow.xaml, App.xaml and App.config I have MainContent1.xaml, MainContent2.xaml, ComboItems.cs and two subfolder Sub1 and Sub2, each of them contains a single file named S1.xaml, in my project.
All of my code is written in ComboItems.cs as -
namespace MultiBinding
{
public class Item
{
public string Name { get; set; }
public int Id { get; set; }
}
public class ComboItems
{
public ComboItems()
{
Items = new List<Item>(3);
for (int i = 1; i < 4; i++)
Items.Add(new Item { Id = i, Name = "Name " + i });
}
public List<Item> Items { get; set; }
}
public class ButtonContent : INotifyPropertyChanged
{
public ICommand MyCommand { get; set; }
private string _content;
public ButtonContent()
{
_content = "First";
MyCommand = new Command(Do, CanDo);
}
public string Content
{
get { return _content; }
set { _content = value; OnChange("Content"); }
}
private bool CanDo(object parameter) => true;
private void Do(object parameter) => Content = Content == "First" ? "Second" : "First";
public event PropertyChangedEventHandler PropertyChanged;
private void OnChange(string name) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
public class Command : ICommand
{
private Action<object> Do;
private Func<object, bool> CanDo;
public Command(Action<object> Do, Func<object, bool> CanDo)
{
this.Do = Do;
this.CanDo = CanDo;
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter) => true;
public void Execute(object parameter) => Do(parameter);
}
public class SomeText
{
public static string Text1 { get; set; }
public static string Text2 { get; set; }
}
public class Converter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
switch (values[1].ToString())
{
case "1": SomeText.Text1 = SomeText.Text2 = "Selected Item is 1"; break;
case "2": SomeText.Text1 = SomeText.Text2 = "Selected Item is 2"; break;
case "3": SomeText.Text1 = SomeText.Text2 = "Selected Item is 3"; break;
}
if (values[0].ToString() == "First") return new MainContent1();
else return new MainContent2();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
Content of MainWindow.xaml is -
<Window x:Class="MultiBinding.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MultiBinding">
<Window.Resources>
<local:ComboItems x:Key="cboItems" />
<local:Item x:Key="Item" />
<local:Converter x:Key="convert" />
<local:ButtonContent x:Key="content"/>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="10*"/>
<RowDefinition Height="90*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<ComboBox x:Name="combo"
DataContext="{Binding Source={StaticResource Item}}"
ItemsSource="{Binding Path=Items, Source={StaticResource cboItems}}"
DisplayMemberPath="Name"
SelectedValuePath="Id"
SelectedValue="{Binding Path=Id}"/>
<Button x:Name="butt" Grid.Column="1"
DataContext="{Binding Source={StaticResource content}}"
Content="{Binding Path=Content}"
Command="{Binding Path=MyCommand}"/>
<ContentControl Grid.Row="1" Grid.ColumnSpan="2">
<ContentControl.Content>
<MultiBinding Converter="{StaticResource convert}">
<Binding ElementName="butt" Path="Content"/>
<Binding ElementName="combo" Path="SelectedValue"/>
</MultiBinding>
</ContentControl.Content>
</ContentControl>
</Grid>
Content of MainContent1.xaml is -
<UserControl x:Class="MultiBinding.MainContent1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sub="clr-namespace:MultiBinding.Sub1">
<UserControl.Resources>
<sub:S1 x:Key="s1"/>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock FontSize="20" Text="On Main ContentControl No. 1"/>
<ContentControl
Content="{Binding Source={StaticResource s1}}"
Grid.Row="1"/>
</Grid>
MainContent2.xaml contains exact same code as above except
xmlns:sub="clr-namespace:MultiBinding.Sub2"
Content of S1.xaml under folder Sub1 is -
<UserControl x:Class="MultiBinding.Sub1.S1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:main="clr-namespace:MultiBinding">
<UserControl.Resources>
<main:SomeText x:Key="MyText"/>
</UserControl.Resources>
<Grid Background="Bisque">
<TextBlock Text="{Binding Text1, Source={StaticResource MyText}}" />
</Grid>
S1.xaml under folder Sub2 is similar to that of Sub1 folder except
<TextBlock Text="{Binding Path=Text2, Source={StaticResource MyText}}" />
Everything works as expected in this way.
Is there any problem using static keyword infront of Text1 and Text2 properties of SomeText Class in ComboItems.cs?
How can I achieve same functionality without using static properties of SomeText class?
I have removed "ComboItems and Converter" classes from ComboItems.cs, renamed "ButtonContent" class as "MyCode" and implemented it as follows -
public class MyCode : INotifyPropertyChanged
{
//Fields
private Sub1.S1 _s1;
private Sub2.S1 _s2;
private MainContent1 _m1;
private MainContent2 _m2;
private object _mainContent;
private object _subContent;
private int _selectedIndex;
//Properties
public string Text { get; set; }
public List<Item> Items { get; set; }
public ICommand MyCommand { get; set; }
public string Content { get; set; }
public object MainContent
{
get { return _mainContent; }
set { _mainContent = value; OnChange("MainContent"); }
}
public object SubContent
{
get { return _subContent; }
set { _subContent = value; OnChange("SubContent"); }
}
public int SelectedIndex
{
get { return _selectedIndex; }
set
{
_selectedIndex = value;
//Action on selection change
switch (_selectedIndex)
{
case 0: Text = "Selected Item is 1"; break;
case 1: Text = "Selected Item is 2"; break;
case 2: Text = "Selected Item is 3"; break;
}
OnChange("Text");
}
}
//Constructor
public MyCode()
{
Content = "First";
MyCommand = new Command(Do, CanDo);
_m1 = new MainContent1();
_m2 = new MainContent2();
_s1 = new Sub1.S1();
_s2 = new Sub2.S1();
MainContent = _m1;
SubContent = _s1;
Items = new List<Item>(3);
for (int i = 1; i < 4; i++)
Items.Add(new Item { Id = i, Name = "Name " + i });
SelectedIndex = 0;
}
//Action on Button Click
private void Do(object parameter)
{
if(Content == "First")
{
MainContent = _m2;
SubContent = _s2;
Content = "Second";
}
else
{
MainContent = _m1;
SubContent = _s1;
Content = "First";
}
OnChange("Content");
}
private bool CanDo(object parameter) => true;
//Inotify
public event PropertyChangedEventHandler PropertyChanged;
private void OnChange(string name) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
Changed MainWindow.xaml to -
<Window.Resources>
<local:MyCode x:Key="Code"/>
</Window.Resources>
<Grid DataContext="{Binding Source={StaticResource Code}}">
<Grid.RowDefinitions...>
<Grid.ColumnDefinitions...>
<ComboBox ItemsSource="{Binding Path=Items}"
DisplayMemberPath="Name"
SelectedValuePath="Id"
SelectedIndex="{Binding Path=SelectedIndex}"/>
<Button x:Name="butt" Grid.Column="1"
Content="{Binding Path=Content}"
Command="{Binding Path=MyCommand}"/>
<ContentControl Grid.Row="1" Grid.ColumnSpan="2"
Content="{Binding Path=MainContent}"/>
</Grid>
Both of MainContent1.xaml and MainContent2.xaml now contains -
<ContentControl Content="{Binding Path=SubContent}"/>
And S1.xaml under folder Sub1 and Sub2 contains -
<TextBlock Text="{Binding Path=Text}" />
Done!
I have a small problem with my MVVM application.
I have a function in a viewmodel which modify a collection. This collection is bind to the view to show a datagrid. When an user click on a button, the function modify the collection but it can take few minutes and the view is not refresh.
My question is how can I execute this function to have the view refreshed in real time ?
In another program I have used the dispatcher but it was in the code behind of the view without binding.
Thanks
Edit :
Model :
public class Composants : INotifyPropertyChanged
{
private string _nom;
public string Nom
{
get { return _nom; }
set { _nom = value; OnPropertyChanged("Nom"); }
}
}
ViewModel :
public class PageSynchroViewModel : INotifyPropertyChanged
{
public void SynchroniserComposants()
{
foreach (var comp in _selectedVersion.ListeComposants)
{
comp.Nom = "";
}
}
View (I don't put all the code):
<Page x:Class="Centre_de_synchronisation.Vues.PageSynchro"
[...]
xmlns:app="clr-namespace:Centre_de_synchronisation.Classes" mc:Ignorable="d"
d:DesignHeight="531" d:DesignWidth="778"
Title="PageSynchro" Background="{x:Null}">
<Canvas>
[...]
<DataGrid Name="GridComposants" Style="{StaticResource DatagridStyle}" ItemsSource="{Binding ListeComposants}" AutoGenerateColumns="False" Canvas.Left="12" Canvas.Top="201" Height="285" Width="754" >
<DataGrid.Columns>
<DataGridTextColumn
Header="Nom"
Binding="{Binding Nom}"
Width="150"
IsReadOnly="True"/>
[...]
</DataGrid>
<Button Name="BoutonSynchro" Style="{StaticResource MessageBoxButtonStyle}" Content="Synchroniser" Height="27" Width="107" Command="{Binding BoutonSynchro}" CommandParameter="GridComposants" Visibility="{Binding Etat, Converter={StaticResource VisibilityConverter}}"/>
</Canvas>
Try using an ObservableCollection<T> instead of the collection you are using now.
This should cause the View to be updated whenever an Item is added or removed from the collection.
Just remember when interacted with the ObservableCollection to Invoke the Dispatcher otherwise you will get Thread Access Exceptions
Here is the code I did to test this.
XAML
<Window.Resources>
<loc:MyViewModel x:Key="ViewModel" />
</Window.Resources>
<Canvas DataContext="{StaticResource ViewModel}">
<DataGrid ItemsSource="{Binding Collection}"
Width="150"
Height="200">
<DataGrid.Columns>
<DataGridTextColumn Header="Nom"
Binding="{Binding Nom}"
Width="150"
IsReadOnly="True" />
</DataGrid.Columns>
</DataGrid>
<Button Command="{Binding DoStuffCommand}"
Canvas.Bottom="0"
Canvas.Right="0">Stuff</Button>
</Canvas>
ViewModel
public class MyViewModel
{
public ObservableCollection<MyModel> Collection { get; set; }
public ICommand DoStuffCommand { get; set; }
public MyViewModel()
{
this.Collection = new ObservableCollection<MyModel>();
for (int i = 0; i < 10; i++)
{
Collection.Add(new MyModel { Nom = i.ToString() });
}
this.DoStuffCommand = new RelayCommand(DoStuff);
}
private void DoStuff()
{
foreach (var item in Collection)
{
item.Nom = item.Nom + ".";
}
}
}
Model
public class MyModel : INotifyPropertyChanged
{
private string nom;
public string Nom
{
get { return nom; }
set
{
nom = value;
RaiseChanged("Nom");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaiseChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
It updated the Nom in the view.
I have a ComboBox which is has an ItemTemplate applied on it and is bind to a List of entity return using linq. I'm using mvvm. It is bind to it successfully but when I set the selected value of it from code at runtime to show the selected value coming from db it doesn't select it. For reference here is my ComboBox xaml.
<DataTemplate x:Key="ManufacturerDataTemplate">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Image x:Name="imgManufacturer" Width="25" Height="25"
Source="{Binding Path=ManufacturerImage}" Grid.Column="0"/>
<TextBlock x:Name="txtManufacturer" Grid.Column="1" HorizontalAlignment="Left"
VerticalAlignment="Center" Text="{Binding Path=ManufacturerName}"
Tag="{Binding Path=ManufacturerID}"/>
</Grid>
</DataTemplate>
<ComboBox x:Name="cboManufacturer"
SelectionChanged="cboManufacturer_SelectionChanged"
ItemsSource = "{Binding Path=CurrentManufacturers}"
SelectedValue="{Binding Path=SelectedManufacturer}"
Grid.Column="3" Grid.Row="2" Margin="20,9.25,68,7.75"
ItemTemplate="{StaticResource ManufacturerDataTemplate}" TabIndex="6"/>
Here is my part from code behind from viewModel.
List<tblManufacturer> currentManufacturers
= new List<tblManufacturer>();
tblManufacturer selectedManufacturer = null;
public List<tblManufacturer> CurrentManufacturers
{
get
{
return currentManufacturers;
}
set
{
currentManufacturers = value;
NotifyPropertyChanged("CurrentManufacturers");
}
}
public tblManufacturer SelectedManufacturer
{
get
{
return selectedManufacturer;
}
set
{
selectedManufacturer = currentManufacturers.Where(mm => mm.ManufacturerID == Convert.ToInt32(selectedDevice.tblManufacturer.EntityKey.EntityKeyValues[0].Value)).First();
NotifyPropertyChanged("SelectedManufacturer");
}
}
Here is the sample code snippet:
Xaml for ComboBox:
<ComboBox ItemsSource="{Binding ManufacturerList}" DisplayMemberPath="Name" SelectedValuePath="ID"
SelectedItem="{Binding SelectedManufacturer}"/>
ViewModel code :
public class Manufacturer
{
public int ID { get; set; }
public string Name { get; set; }
}
private List<Manufacturer> _manufactuerlist;
private Manufacturer _selectedManufacturer;
private void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
public Manufacturer SelectedManufacturer
{
get
{
return _selectedManufacturer;
}
set
{
_selectedManufacturer = value;
NotifyPropertyChanged("SelectedManufacturer");
}
}
public List<Manufacturer> ManufacturerList
{
get
{
return _manufactuerlist;
}
set
{
_manufactuerlist = value;
NotifyPropertyChanged("ManufacturerList");
}
}
And finally Set the Selected Manufacturer in your view model like this:
SelectedManufacturer = _manufactuerlist.Find(m => m.ID == 2);
I am trying to successfully TwoWay bind an ObservableCollection to TextBoxes in a DataTemplate. I can get the data to display properly, but I am unable to change the list data through the UI. I have a Model class named 'model' which contains an ObservableCollection named 'List'. The class implements the INotifyPropertyChanged interface. Here is the xaml for the shell. The DataContext for Window1's grid is set to "theGrid.DataContext=model"
<Window x:Class="BindThat.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:BindThat"
Title="Window1" Height="300" Width="300">
<StackPanel x:Name="theGrid">
<GroupBox BorderBrush="LightGreen">
<GroupBox.Header>
<TextBlock Text="Group" />
</GroupBox.Header>
<ItemsControl ItemsSource="{Binding Path=List}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBox Text="{Binding Path=., Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</GroupBox>
</StackPanel>
This is the code for the Model class:
class Model : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string name)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
private ObservableCollection<string> _list = new ObservableCollection<string>();
public ObservableCollection<string> List
{
get { return _list; }
set
{
_list = value;
NotifyPropertyChanged("List");
}
}
public Model()
{
List.Add("why");
List.Add("not");
List.Add("these?");
}
}
Could anyone advise if I am going about this the correct way?
You need a property to bind two way, so string is not good for this.
Wrap it in a string object, like this:
public class Model
{
public ObservableCollection<StringObject> List { get; private set; }
public Model()
{
List = new ObservableCollection<StringObject>
{
new StringObject {Value = "why"},
new StringObject {Value = "not"},
new StringObject {Value = "these"},
};
}
}
public class StringObject
{
public string Value { get; set; }
}
and bind to Value property instead of "."
Also, you don't need to notify of a change in observable collection, so until your model has some other propertis of its own, it does not need to have INotifyPropertyChange. If you want your ItemsControl react to changes in the individual StringObjects, then you should add INotifyPropertyChanged to a StringObject.
And yet again, two way binding is default, so you need only
<TextBox Text="{Binding Path=Value}" />
in your binding.
I believe you need to derive your collection items from DependencyObject for TwoWay binding to work. Something like:
public class DependencyString: DependencyObject {
public string Value {
get { return (string)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(string), typeof(DependencyString), new UIPropertyMetadata(""));
public override string ToString() {
return Value;
}
public DependencyString(string s) {
this.Value = s;
}
}
public class Model {
private ObservableCollection<DependencyString> _list = new ObservableCollection<DependencyString>();
public ObservableCollection<DependencyString> List {
get { return _list; }
}
public Model() {
List.Add(new DependencyString("why"));
List.Add(new DependencyString("not"));
List.Add(new DependencyString("these?"));
}
}
...
<StackPanel x:Name="theGrid">
<GroupBox BorderBrush="LightGreen">
<GroupBox.Header>
<TextBlock Text="Group" />
</GroupBox.Header>
<ItemsControl ItemsSource="{Binding Path=List}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBox Text="{Binding Path=Value, Mode=TwoWay}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</GroupBox>
</StackPanel>
xaml view:
<ItemsControl ItemsSource="{Binding List}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBox Text="{Binding Path=Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
in code behind in the constructor:
DataContext = new ViewModel();
in ViewModel Class:
class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string name)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
private ObservableCollection<StringObject> _List = new ObservableCollection<StringObject>();
public ObservableCollection<StringObject> List
{
get { return _List; }
set
{
_List = value;
NotifyPropertyChanged("List");
}
}
public ViewModel()
{
List = new ObservableCollection<StringObject>
{
new StringObject {Value = "why"},
new StringObject {Value = "not"},
new StringObject {Value = "these"}
};
}
}
public class StringObject
{
public string Value { get; set; }
}
Be careful with a collection with type string it doesn't work, you have to use an object => StringObject