I'm not understanding why my combo box doesn't update when I make a selection in another combobox. I'm still new to MVVM but in theory my code should work. I can populate the combos when the form loads but i need to refresh the combo with new values, and that's not working. I can see it retrieving new values but it never displays them on the form.
My XAML looks like this:
<ComboBox Grid.Column="1" ItemsSource="{Binding Path=Vendors}" SelectedItem="{Binding SelectedVendor, Mode=TwoWay}" HorizontalAlignment="Left" Margin="24,12,0,11" Grid.Row="3" VerticalAlignment="Center" Width="293" />
<ComboBox x:Name="VendorProductServiceCB" HorizontalAlignment="Left" Margin="20.6,16.2,0,55.4" VerticalAlignment="Center" Width="293" Grid.Row="7" Grid.Column="1" ItemsSource="{Binding Path=VendorProductServices, UpdateSourceTrigger=PropertyChanged}" DisplayMemberPath="Name" SelectedItem="{Binding Path=SelectedVendorProductService, Mode=TwoWay}" Height="22"/>
My ViewModel code is this:
public ObservableCollection<string> Vendors { get; set; }
public ObservableCollection<VendorProductService> VendorProductServices { get; private set; }
public VendorProductService SelectedVendorProductService
{
get { return _selectedVendorProductService; }
set { SetProperty(ref _selectedVendorProductService, value); }
}
public string SelectedVendor
{
get { return _selectedVendor; }
set
{
SetProperty(ref _selectedVendor, value);
SelectionChangedCommand.Execute(this);
}
}
public FormSelectionViewModel()
{
Vendors = new ObservableCollection<string>(FetchVendors());
VendorProductServices = new ObservableCollection<VendorProductService>(FetchVendorProductServices(_selectedVendor));
SelectionChangedCommand = new DelegateCommand(SelectionChanged);
}
public void SelectionChanged()
{
VendorProductServices = new ObservableCollection<VendorProductService>(FetchVendorProductServices(_selectedVendor));
}
You're not raising the PropertyChanged event for the VendorProductServices, because it's an auto property.
Either change it to:
private ObservableCollection<VendorProductService> _vendorProductServices;
public ObservableCollection<VendorProductService> VendorProductServices
{
get { return _vendorProductServices; }
private set { SetProperty(ref _vendorProductServices, value); }
}
Or change your collection properties to read only and use .Clear() and .Add() instead of creating new collections.
Related
I'm making a ListView filled with List of objects, which properties are shown and editable in a ListView. I need to get object when its properties are being updated. How can I do this?
I tried creating an object of class and bind it to SelectedItem in ListView. The problem is that, obviously, the SelectedItem is set after clicking the row of ListItem, but not the children of that row. I need to get the updated object from the row of my ListView each time after any ComboBox or TextBox values are changed.
To handle all the things with INotifyPropertyChanged I'm using PropertyChanged.Fody. Could it help me to solve this problem easier?
View
Appearance of the ListView
<ListView
Margin="10"
Grid.Row="1"
Grid.ColumnSpan="2"
ItemsSource="{Binding TimesheetEntries}"
SelectedItem="{Binding SelectedEntry, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
>
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Height="30" Margin="3">
<TextBlock
Text="{Binding Date, StringFormat=dd-MM-yyyy}"
VerticalAlignment="Center"
Width="Auto"
Margin="10"/>
<ComboBox
SelectedValuePath="Key" DisplayMemberPath="Value"
ItemsSource="{Binding EmploymentTypesDictionary, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectedValue="{Binding SelectedEmployment, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Width="270"/>
<TextBox
Text="{Binding Hours, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Margin="10,0,0,0"
Width="70"/>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
ViewModel
public List<TimesheetEntryEntity> TimesheetEntries
{
get { return _TimesheetEntries; }
set { _TimesheetEntries = value; }
}
public TimesheetEntryEntity SelectedEntry
{
get { return _SelectedEntry; }
set { _SelectedEntry = value; }
}
...
private List<TimesheetEntryEntity> _TimesheetEntries { get; set; }
private TimesheetEntryEntity _SelectedEntry;
private TimesheetModel timesheetModel;
public TimesheetViewModel()
{
this.Timesheets = TimesheetUnitModel.GetAllTimesheetsForUnit((int)Application.Current.Properties["UnitID"]);
this._StartDate = DateTime.Now;
_TimesheetEntries = new List<TimesheetEntryEntity>();
}
public KeyValuePair<int, string> SelectedWorker
{
get { return _SelectedWorker; }
set
{
_SelectedWorker = value;
_TimesheetEntries =
timesheetModel.GetTimesheetList(_SelectedWorker.Key, SelectedTimesheet.Key, StartDate.Date);
}
}
TimesheetEntryEntity
public DateTime Date { get; set; }
public Dictionary<EmploymentTypes, string> EmploymentTypesDictionary { get; set; }
public EmploymentTypes SelectedEmployment {
get { return _SelectedEmployment; }
set
{
_SelectedEmployment = value;
CheckHoursAvaliability();
}
}
public bool HoursAvaliable { get; set; }
public decimal Hours
{
get;
set;
}
private EmploymentTypes _SelectedEmployment;
public TimesheetEntryEntity()
{
FillEmploymentTypes();
}
public void FillEmploymentTypes()
{
//Some code here
}
I tried to follow the answer from Get Object properties of selected list item question, but there were only textblocks, so the row gets selected anyway, but i have ComboBox and TextBox, who get their own focus.
You can implement INotifyPropertyChanged in your TimesheetEntryEntity i.e.
public abstract class TimesheetEntryEntity: INotifyPropertyChanged
{
public event EventHandler Changed;
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public void OnChange()
{
EventHandler handler = Changed;
handler?.Invoke(this, EventArgs.Empty);
}
private DateTime date;
public DateTime Date
{
get => date;
set
{
if (date == value)
{
return;
}
//Do something with unchanged property
date = value;
RaisePropertyChanged();
OnChange();
//Do something with changed property
}
}
in your ViewModel before adding new item to list:
timesheet.Changed+=ItemChanged;
and
private void ItemChanged(object sender, EventArgs e)
{
var item=sender as TimesheetEntryEntity;
//do something
}
I have the ListBox on my MainView.xaml, selecting the Item forces the ContentControl to display different UserControls. I use Caliburn.Micro library in this propgram. Here's some code:
<ListBox Grid.Row="1" Grid.Column="1" x:Name="ItemsListBox" SelectedItem="0" ItemsSource="{Binding Items}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding TextBlock1Text}" x:Name="TextBlock1"/>
<ContentControl Grid.Row="3" Grid.Column="1" Content="{Binding ElementName=ItemsListBox, Path=SelectedItem.Content}" />
The MainViewModel.cs:
private string _name;
public string Name
{
get => _name;
set
{
_name = value;
NotifyOfPropertyChange(() => Name);
}
}
private string _textBlock1Text;
public string TextBlock1Text
{
get => _textBlock1Text;
set
{
_textBlock1Text = value;
NotifyOfPropertyChange(() => TextBlock1Text);
}
}
public MainViewModel()
{
TextBlock1Text = "Test";
Items = new ObservableCollection<ItemsModel>()
{
new ItemsModel { Name="Useless", Content=null },
new ItemsModel { Name="TextChangerViewModel", Content=new TextChangerViewModel(TextBlock1Text) }
};
}
public ObservableCollection<ItemsModel> Items { get; set; }
The ItemsModel.cs:
public class ItemsModel
{
public string Name { get; set; }
public object Content { get; set; }
}
And finally the TextChangerViewModel.cs:
public class TextChangerViewModel : Conductor<object>
{
private string _textBlock1Text;
public string TextBlock1Text
{
get => _textBlock1Text;
set
{
_textBlock1Text = value;
NotifyOfPropertyChange(() => TextBlock1Text);
}
}
public TextChangerViewModel(string textBlock1Text) //passing parameter from another ViewModel
{
TextBlock1Text = textBlock1Text;
}
}
So, the main question is how to change the TextBlock1Text (and the Text value of TextBlock in .xaml as well) in the MainViewModel.cs from the TextChangerViewModel.cs? I was thinking about using something like NotifyCollectionChanged on my Items ObservableCollection, but it work with collection of ItemsModel, not with the VM's, so I'm stuck here.
I'm also not sure if having public object Content { get; set; } in ItemsModel.cs is a good thing if I'm targeting the MVVM pattern, but I don't know the other way to do it (I'm very new to MVVM).
UPD
I'm looking for the property-changing way because I need to change the TextBlock1Text Text from another UserControl. Suppose I have the button on my TextChangerView.xaml: <Button Grid.Row="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Content="Change da text" cal:Message.Attach="ChangeTextButton"/>
And after the click on it I want the text on the parental MainView.xaml to change. But the thing is, I don't know how to change properties in this case, as I wrote above why.
Change the the binding of textblox1 to reference the selected item.
<TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding ElementName=ItemsListBox, Path=SelectedItem.Name}" x:Name="TextBlock1"/>
or
<TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding ElementName=ItemsListBox, Path=SelectedItem.Content.TextBlock1Text}" x:Name="TextBlock1"/>
My issue is that UI is not updated even when PropertyChanged is fired.
XAML:
<ListBox Name="BookShelf" Visibility="Hidden" SelectedItem="{Binding SelectedItem}" Panel.ZIndex="1" Height="Auto" Grid.Column="3" Margin="8,50,0,0" HorizontalAlignment="Center" ItemsSource="{Binding BookShelf}" Background="Transparent" Foreground="Transparent" BorderThickness="0" BorderBrush="#00000000">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel VerticalAlignment="Center" Orientation="Vertical">
<TextBlock FontSize="14" Margin="0,10,0,0" FontWeight="Bold" Foreground="Black" HorizontalAlignment="Center" Text="{Binding Path=DbId}" />
<TextBlock FontSize="16" FontWeight="Bold" Width="170" TextWrapping="Wrap" Foreground="Black" Margin="5" HorizontalAlignment="Center" Text="{Binding Path=DisplayName}" />
<Image HorizontalAlignment="Center" Source="{Binding Path=bookImage}" Width="200" Height="200" Margin="0,0,0,10" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
And:
<ComboBox Margin="8,15,0,0" Name="bookShelf_ComboBox" ItemsSource="{Binding BookShelf}" SelectedItem="{Binding SelectedItem}" VerticalAlignment="Center" HorizontalAlignment="Center" DisplayMemberPath="DisplayName" Height="22" Width="140" Visibility="Visible" SelectionChanged="bookShelf_ComboBox_SelectionChanged"/>
Viewmodel:
public class BookShelfViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public event ShowContentInBrowser ShowDatabaseContent;
public BookShelfViewModel(ShowContentInBrowser showMethod)
{
ShowDatabaseContent += showMethod;
}
private ObservableCollection<DbInfo> _BookShelf = new ObservableCollection<DbInfo>();
public ObservableCollection<DbInfo> BookShelf
{
get
{
if (_BookShelf == null)
_BookShelf = new ObservableCollection<DbInfo>();
return _BookShelf;
}
set
{
if (value != _BookShelf)
_BookShelf = value;
}
}
private DbInfo _selectedItem { get; set; }
public DbInfo SelectedItem
{
get
{
return _selectedItem;
}
set
{
if (_selectedItem != value)
{
_selectedItem = value;
RaisePropertyChanged(new PropertyChangedEventArgs("SelectedItem"));
if (_selectedItem == null)
return;
if (_selectedItem.RelatedId != null)
ShowDatabaseContent(_selectedItem, _selectedItem.RelatedId);
else
ShowDatabaseContent(_selectedItem, _selectedItem.RelatedId);
}
}
}
public void RaisePropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
PropertyChanged(this, e);
}
}
This code I'm using is for setting DataContext and SelectedItem:
await System.Windows.Application.Current.Dispatcher.BeginInvoke(
DispatcherPriority.Background, new Action(
() => this.BookShelf.SelectedItem = dbInfo
)
);
And DataContext:
await System.Windows.Application.Current.Dispatcher.BeginInvoke(
DispatcherPriority.Background, new Action(
() => this.BookShelf.DataContext = bookShelfViewModel
)
);
I'm very new for this MVVM design and as far as I have can tell from articles I have read, I cant find what's wrong. I'm guessing that using Dispatcher is not necessary but I don't think it matters in this case...
ListBox does show my objects but updating SelectedItem is the issue here...
UPDATE:
Heres my code for DbInfo:
public class DbInfo
{
public int RelatedId { get; set; }
public string DbId { get; set; }
public TBase3.Article currentArticle { get; set; }
public string LinkId { get; set; }
public bool IsArticle { get; set; }
public string folder { get; set; }
public bool IsNamedArticle { get; set; }
public int currentBlockIndex { get; set; }
public int currentBlockCount { get; set; }
public string DisplayName { get; set; }
public int VScrollPos { get; set; }
public int THTextVersion { get; set; }
public bool isHtmlToc { get; set; }
public ImageSource bookImage { get; set; }
}
Reminder that when ever I set new value for ViewModel -> SelectedItem and It goes to PropertyChanged(this, e); line. It does not Selected that DbInfo as Selected in ListBox.
Update2:
I got right side of my window a list of books, like a Book Shelf many books in it.
It shows all book with scroll. Book which is selected its content is being shown in window.
But If for reason I want to change to another book from code-behind, it updates it content to webbrowser but not update ListBox that certain book as SelectedItem
ANSWER:
Okay I found the answer right now. Code which set BookShelf.SelectedItem = dbInfo should be bookShelfViewModel.SelectedItem = bookShelfViewModel.BookShelf.First(x => x.DbId == dbInfo.DbIf);
await System.Windows.Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() => this.BookShelf.DataContext = bookShelfViewModel));
That does not look good, where do you do this? I would reccomend the use of Galasoft MVVM Light and the usage of a ViewModelLocator, for setting your DataContext(avaliable through nuget).It provides you with a ViewModelBase, with all your propertychanged needs and the works, which you may extend to suit your neeeds. It sounds like the DataContext, may be the actual problem if PropertyChanged isn't even raised.
EDIT:
As pointed out by Clemens the Mode=TwoWay in the binding is not needed here, as it is the default for the SelectedItem property anyway, I'll just leave it as an example....
<ComboBox Margin="8,15,0,0" Name="bookShelf_ComboBox" ItemsSource="{Binding BookShelf}" SelectedItem="{Binding SelectedItem, Mode=TwoWay}" VerticalAlignment="Center" HorizontalAlignment="Center" DisplayMemberPath="DisplayName" Height="22" Width="140" Visibility="Visible">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<command:EventToCommand Command="{Binding SelectedItemChangedCommand}" PassEventArgsToCommand="True"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</ComboBox>
I noticed your SelectionChanged="bookShelf_ComboBox_SelectionChanged" in the code, this is "nasty", use event to command instead, because you are tying your viewmodel to your views cbox. The code above will execute a command with arguments included, add the following to your viewmodel.
public ICommand SelectedItemChangedCommand{ get; set;}
// somewhere in your code..
SelectedItemChangedCommand = new RelayCommand<SelectedItemChangedCommand>(OnSelectedItemChanged);
protected void OnSelectedItemChanged(SelectedItemChangedCommand e)
{
// Do your magic here! Or why not just call this method from the setter of your bound property?
}
The WPF Cheat Sheet is a nice compact list of all types of bindings, which is very handy, I used to have it both at wall and home, when I was learning WPF :)
Hope it helps
Cheers
Stian
what you mean by the UI is not updated? you set a new ItemsSource and dont see any Changes?
if that is the case change your Property to
public ObservableCollection<DbInfo> BookShelf
{
get
{
if (_BookShelf == null)
_BookShelf = new ObservableCollection<DbInfo>();
return _BookShelf;
}
set
{
if (value != _BookShelf)
{ _BookShelf = value;
RaisePropertyChanged(new PropertyChangedEventArgs("BookShelf"));
}
}
btw i use ObservableCollection in another way. i just initialize it once and use Clear, Add, Remove.
If the ItemsSource is not your Problem pls post the Code for DbInfo, and write something more to your "UI is not updated" problem
Okay I found the answer right now. Code which set BookShelf.SelectedItem = dbInfo should be bookShelfViewModel.SelectedItem = bookShelfViewModel.BookShelf.First(x => x.DbId == dbInfo.DbIf);
Hi i need to make a program where you have to add an undefined number of elements to a list by choosing them from a combobox. I planned to use 4 basic comboboxes and when the user choose an element from the last one, the program should automatically add another one under the last one (i want to use a stackpanel).
How could i do?
Thanks.
My XAML:
<StackPanel Name="listPanel" Grid.Column="0" Margin="10">
<Label Content="Example" FontWeight="Bold" HorizontalAlignment="Center"/>
<ComboBox Name="ex1Combobox" Margin="0,10,0,0"
ItemsSource="{Binding ExList, Mode=TwoWay}"
SelectedValue="{Binding SelectedEx}"
DisplayMemberPath="Name"
SelectedValuePath="ID"/>
<ComboBox Name="ex2Combobox" Margin="0,10,0,0"
ItemsSource="{Binding ExList, Mode=TwoWay}"
SelectedValue="{Binding SelectedEx}"
DisplayMemberPath="Name"
SelectedValuePath="ID"/>
<ComboBox Name="ex3Combobox" Margin="0,10,0,0"
ItemsSource="{Binding ExList, Mode=TwoWay}"
SelectedValue="{Binding SelectedEx}"
DisplayMemberPath="Name"
SelectedValuePath="ID"/>
</StackPanel>
This is a pretty good example of why you should use MVVM.
Model
Has a collection of selected values only something like
public class MyChoices
{
public IEnumerable<string> Selections {get; set;}
}
ViewModel
Has a collection that extends as soon as you modify the last item
public class MyChoicesViewModel
{
public MyChoicesViewModel()
{
Selections = new ObservableCollection<ChoiceViewModel>();
//Add first empty value
AddNewItem();
Selections.CollectionChanged += (sender, e) =>
{
// If you change the last add another
if (e.NewItems.Contains(Selections.Last()))
AddNewItem();
};
}
public ObservableCollection<ChoiceViewModel> Selections {get; private set;}
public void AddNewItem()
{
var newItem = new ChoiceViewModel();
Selections.Add(newItem);
newItem.PropertyChanged += () =>
{
//This is where we update the model from the ViewModel
Model.Selections = from x in Selections
select x.Value;
}
}
}
public class ChoiceViewModel : INotifyPropertyChanged
{
private string _chosen;
public string Chosen
{
get { return _chosen; }
set {
if (_chosen != value)
{
_chose = value;
OnPropertyChanged();
}
}
}
public void OnPropertyChanged([CallerMemberName] string property)
{
var temp = PropertyChanged;
if (temp != null)
{
temp(this, new PropertyChangedEventArgs(property));
}
}
}
}
View
<!-- Then show many of them-->
<ListBox ItemsSource="{Binding Selections}"/>
I have a problem in c# wpf tring to bind to CurrentItem, i have a list of persons, and each persons can have one of two items. You can select a person in the list and then select it's item in a combobox.
The combobox binds to Persons.CurrentItem.Item and shows what the person have as selected item. But i cant change it, or rather i cant keep the change that is made, it changes back as soon as i select a new person.
The XAML looks like this:
<!--This dose not work-->
<TextBox Height="23" HorizontalAlignment="Left" Margin="148,55,0,0" Name="textBox1" VerticalAlignment="Top" Width="120" Text="{Binding Path=Persons.CurrentItem.Item.Name}"/>
<!--This works-->
<TextBox Height="23" HorizontalAlignment="Left" Margin="16,306,0,0" Name="textBox2" VerticalAlignment="Top" Width="120" Text="{Binding Path=Persons.CurrentItem.Name}"/>
<!--This works, we do not bind to CurrentItem-->
<ListBox Height="274" HorizontalAlignment="Left" ItemsSource="{Binding Path=Persons2}" SelectedItem="{Binding Path=SelectedPerson}" Margin="292,26,0,0" Name="listBox2" VerticalAlignment="Top" Width="120" DisplayMemberPath="Name" />
<ComboBox Height="23" HorizontalAlignment="Left" ItemsSource="{Binding Path=Items}" SelectedItem="{Binding Path=SelectedPerson.Item}" Margin="431,26,0,0" Name="comboBox2" VerticalAlignment="Top" Width="120" DisplayMemberPath="Name"/>
</Grid>
As you can see i have added a persons2 with SelectedItem as SelectedPerson. This works fine and i want to mimic it's function but i want to use Current item.
This is the C# code:
// Selectable items
public List<Item> Items { get; set; }
// List of persons, we will bind to it's CurrentItem
public List<Person> Persons { get; set; }
// This works, we do not use CurrentItem
public List<Person> Persons2 { get; set; }
private Person _selectedPerson;
public Person SelectedPerson
{
get { return _selectedPerson; }
set
{
_selectedPerson = value;
NotifyPropertyChanged("SelectedPerson");
}
}
#region Constructo
public Window()
{
InitializeComponent();
DataContext = this;
// Populate Items
Items = new List<Item>
{
new Item {Name = "Hammer"},
new Item {Name = "Axe"}
};
// Populate persons
Persons = new List<Person>() { new Person { Name = "Lisa", Item = Items.FirstOrDefault()}, new Person { Name = "Kauf" } };
Persons2 = new List<Person>(Persons); // make a copy
}
#endregion
#region PropertyChangeHandler
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string name)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(name));
}
#endregion
}
public class Item
{
public string Name { get; set; }
}
public class Person
{
public string Name { get; set; }
private Item _item;
public Item Item
{
get { return _item; }
set
{
// We only accass this if we do not bind to CurrentItem
_item = value;
}
}
}
If you test the example you can see that Persons.CurrentItem.Name works, but Persons.CurrentItem.Item.Name dose not, Why? Have i missed something with the level of access?
Is there something i have missed on how to use CurrentItem?
Thanks for enlightening me.
Asked the same question on msdn:
http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/18eee956-3a91-4db3-a4ff-7e8d127ae301
The solution is not use CurentItem but instead / like this:
<StackPanel>
<ListBox ItemsSource="{Binding Path=Persons}" IsSynchronizedWithCurrentItem="True" DisplayMemberPath="Name" />
<ComboBox HorizontalAlignment="Left" VerticalAlignment="Top" ItemsSource="{Binding Path=Items}" SelectedItem="{Binding Persons/Item}" DisplayMemberPath="Name"/>
</StackPanel>