I have a form with two ComboBoxes. One of them is being filled with objects coming from a collection in the ViewModel. When I select a value in this ComboBox, it then should fill the second ComboBox.
What I want to know is what the best way is to go about filling the second ComboBox. I think having yet another collection with the details of the selected value of the first ComboBox in the ViewModel might be a bit wasteful. I think the best way might be to hit the database with the selected value, collecting the corresponding details, and then send them back. How I think this would work is to have the details ComboBox have a binding with the 'master' ComboBox so it can get the selected value. Then ideally, the details ComboBox would then somehow get the values from the database.
Problem is that I just don't know how to implement this with MVVM, and any help would be appreciated!
Just call OnPropertyChanged of the details collection once the selected item changes.
You can pre-populate a background dictionary whose key is the possible master items and whose values are a list of detail list.
Note for the below to work you ViewModel must implement INotifyPropertyChanged
e.g.
public class MyViewModel : INotifyPropertyChanged
{
public IEnumerable<MasterOption> MasterList {get;set;}
public IEnumerable<DetailOption> DetailList {get;set;}
Dictionary<MasterOption,List<DetailOption>> DetailLookup;
MasterOption _SelectedMasterOption;
public MasterOption SelectedMasterOption
{
get { return _SelectedMasterOption;}
set
{
_SelectedMasterOption = value;
LoadDetailsList();
OnPropertyChanged("SelectedMasterOption");
}
void LoadDetailsList()
{
InitDictionary();
if (DetailLookup.ContainsKey(SelectedMasterOption))
DetailList = DetailLookup[SelectedMasterOption];
else
DetailList = null;
OnPropertyChanged("DetailList");
}
void InitDictionary()
{
if (DetailLookup == null)
{
//Grab fill the lookup dictionary with information
}
}
}
Create a method in your ViewModel that gets the data for the second combobox and update with BindingExpression in your codebehind.
private void FirstComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
_viewModel.SelectionChange();
BindingExpression bindingExpression = BindingOperations.GetBindingExpression(SecondComboBox, ComboBox.ItemsSourceProperty);
bindingExpression.UpdateTarget();
}
Related
I have a page with two parts :
A Listview with itemsource binded with a class, ObservableCollection "Patients" loaded with "Patient" class.
Under the Listview are Textboxes binded with the selecteditem Patient. Everything works without writing any code in the page, except in selection_changed to scroll to selected item.
A second Listview must display the details "Visites" from the selected "Patient".
The application works in a MVVM Framework with a Viewmodel containing the properties for the page.
The problem is to make the relation between the two ListView. I tried first building the second list "Visites" in the NotifyPropertyChanged event :
if (Patient.ID > 0)
{
LoadVisite(Patient.ID); // fill the details list "Visites"
NotifyPropertyChanged("Visites");
}
No delail is shown when "Patient" is selected.
I tried another solution inserting the list of details in the master class "Patient like this :
public Class Patient
...
public ObservableCollection<ClsVisite> Visites
{
get
{
return _visites;
}
set
{
_visites = value;
}
}
// WDABase class to open the database and load data connection
WDABase wd = new WDABase();
wd.LoadListeVisites(ID, _visites); //ID is the relation beween the two tables
}
}
Now I try to create the Listview detail itemsource in the XAML like this :
<ListView Name="ListeVisites" ItemsSource="{Binding Path=Patient.Visites}" SelectedItem="{Binding Visite}">
No details where shown.
The unique solution I found was to add some code in the selection_changed event of the master Listview like this (in this case the listviews are in two different frame) :
private void ListePatients_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
ListePatients.ScrollIntoView(ListePatients.SelectedItem);
if(ListePatients.SelectedItem != null)
{
var w1 = ((System.Windows.FrameworkElement)App.Current.MainWindow.Content).Parent;
Frame pageVisite = (w1 as MainWindow).Visit;
var w2 = (System.Windows.FrameworkElement)pageVisite.Content;
ListView Lv = (w2 as Visite).ListeVisites;
Lv.ItemsSource = (ListePatients.SelectedItem as ClsPatient).Visites;
}
}
And it's works but is there another elegant solution to bind the itemsource of the details Listview ?
Thanks for helping me.
Jean-Marie
I found the solution to my problem.
The Reason the details were not displayed is because the frame used for the listview is not common with the master frame.
I discover that when I display the details frame the "Patient" object contains only null. I Don't know why but I read about sharing ViewModel and now I use a common one in App.xaml and I Don't need to specify the itemsource for the details Listview in code. I put in XAML itemsource={Binding Patient.Visites} and everything works.
I have a List bound to a (Telerik) GridView. The selected item is a separate variable of type T which is assigned the object of the selected row in the GridView when the user clicks on a row. T is derived from ObservableObject. This means I am using MVVM Light Toolkit.
I need to deselect the row from my ViewModel in certain situations. On the GridView control this works, if the selected item is set to NULL in the ViewModel. Whenever I do this, MVVM reports a crash (NPE). I debugged it and saw that it is failing in ObservableObject.cs. It calls a method
protected bool Set<T>(
Expression<Func<T>> propertyExpression,
ref T field,
T newValue)
and crashes one line before return when calling RaisePropertyChanged(propertyExpression)
I don't know if this is working as designed or not. My problem is, that I need to set the selected Object to NULL in the ViewModel to deselect a row of my GridView in the View. I CANNOT use CodeBehind for the deselection!
Code I have:
public ObservableCollection<ContractTypeDto> ContractTypes { get; private set; }
public ContractTypeDto SelectedContractType
{
get { return _selectedContractType; }
set
{
Set(() => SelectedContractType, ref _selectedContractType, value);
RaisePropertyChanged(() => SelectedContractType);
}
}
When you click on a row in the grid it opens a new UserControl containing lots of details of this record. This control has its own ViewModel. I store the calling view Model (where the selected item is stored). When the page (control) is closed (destroyed) I have to deselect the row in the grid. I call a method like so:
protected void DeselectCallersSelectedItem()
{
if (CallingObject == typeof(ContractTypeListViewModel))
{
var vm = SimpleIoc.Default.GetInstance<ContractTypeListViewModel>();
vm.SelectedContractType = null;
}
}
Any ideas?
To remove the collection you can either set the SelectedItem property to null or clear the SelectedItems.
gridViewName.SelectedItem = null;
gridViewName.SelectedItems.Clear();
Without showing the code, we cannot precisely help you. A solution I think you can do is to implement the INotifyPropertyChanged interface in your view model and bind the selected item to a property of that type. Also check the output window if there is any binding failure.
Is anyone aware of a way to manually enable (turning on the tick) on the Check Boxes within the CheckComboBox for WPFToolkit?
Unfortunately, the Items in the Combo-box are all strings.
I'm trying to enable all flags when "Select All" checkbox is ticked.
This is a rather late response but I thought it best to post this in case it helps someone out. I have used the following approach for the WPFToolkit version:
public class Descriptor : INotifyPropertyChanged
{
private bool isSelected;
public bool IsSelected
{
get
{
return this.isSelected;
}
set
{
if (this.isSelected != value)
{
this.isSelected = value;
// Raise INotifyPropertyChanged
}
}
}
public string Name { get; set; }
}
Create a collection of these and then assign them to the ItemsSource of the CheckComboBox.
To handle select all we have an option labelled: "" as the first item in the collection, then if this item is ticked all the items are de-selected and the all case is handle under the hood. To handle the selection Changed it does involve adding an event to the Descriptor class and firing it each time the IsSelected property is changed.
I eventually tossed out Extended WPFToolkit due to it's inability to access the checkboxes directly.
Instead I created a ComboBox and manually defined Checkboxes within it, which I access directly by name, and there able to implement a "Select All" by using it's [Checked/Unchecked[ event, and use the ComboBox SelectionChanged to show a default value that expresses what has been selected in a CSV format.
Maybe be clunky, but it gets the job done.
PS. I did not need to even bother with a DataTemplate for the ComboBox
One way in the code Behind is
var ComboSelector = MyCheckComboBox as Xceed.Wpf.Toolkit.Primitives.Selector;
foreach(var item in MyCheckComboBox.Items)
ComboSelector.SelectedItems.Add(item);
May be it's a silly (or more than trivial) kinda question, but it seems i just don't know the answer. Here's the case -
I assigned a UserList as the ItemsSource of a combobox. So what i did essentially is assigning a reference type to another.
I cleared the UserList. So now i get the Count of the ItemsSource 0 as well.
I still get the items present in my combobox. And i also can cast the SelectedItem of the combobox to a User object.
Here's the complete code -
public class User
{
public int Id { get; set; }
public string Name { get; set; }
}
public partial class MainWindow : Window
{
private List<User> _userList;
public MainWindow()
{
InitializeComponent();
_userList = new List<User>()
{
new User() {Id = 1, Name = "X"},
new User() {Id = 2, Name = "Y"},
new User() {Id = 3, Name = "Z"}
};
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
this.comboBox1.ItemsSource = _userList;
this.comboBox1.DisplayMemberPath = "Name";
}
private void button1_Click(object sender, RoutedEventArgs e)
{
_userList.Clear();
/* ItemsSource is cleared as well*/
IEnumerable userList = this.comboBox1.ItemsSource;
/*I can still get my User*/
User user = this.comboBox1.SelectedItem as User;
}
}
So, where the items are coming from? What actually happens under-the-hood when i make such binding? Does the control have some kind of cache? It's a royal pain to realize not having such basic ideas. Can anybody explain the behind-the-scene detail?
EDIT : I wrote the code in WPF, but i have the same question for WinForms Combobox.
EDIT : Doesn't a combobox display its items from it's in-memory Datasource? When that datasource contains 0 items, how does it display the items?
When you set an ItemsSource of any ItemsControl it copies the ref to the list into its Items property. Then it subscribes to the OnCollectionChanged event, and creates a CollectionView object. So, on the screen you can see that collectionView.
as I have found in source code ItemCollection holds two lists:
internal void SetItemsSource(IEnumerable value)
{
//checks are missed
this._itemsSource = value;
this.SetCollectionView(CollectionViewSource.GetDefaultCollectionView((object) this._itemsSource, this.ModelParent));
}
How could you get SelectedItem?
This is my assumption from quick look into the source code:
ItemsControl has a collection of "views" and each View sholud store a ref to the item (User instance), because it has to draw data on the screen. So, when you call SelectedItem it returns a saved ref.
Upd about references
Assume there is an User instance. It has the adress 123 in memory. There is a list. It stores references. One of them is 123.
When you set an ItemsSource ItemsControl saves a reference to the list, and creates a Views collection. Each view stores a references to an item. One view stores an address 123.
Then you cleared a list of users. Now list doesn't contains any references to Users. But in memory there is an adrress 123 and there is an instance of User by this adress. Garbage Collector doesn't destroy it, because View has a reference to it.
When you get SelectedItem it returns User instance from the 123 adress.
var user = new User();
var list = new List<User>();
list.Add(user);
list.Clear();
Console.WriteLine(list.Count()); //prints 0 - list is empty
Console.WriteLine(user == null); //prints false. - user instance is sill exists;
In answer to your comment to #GazTheDestroyer ("... why it doesn't get cleared, and how it holds the items?")
In WPF, when you set the ItemsSource property of an ItemsControl, the control will wrap the list of items in a CollectionView, which is a collection type optimised for use by the UI framework. This CollectionView is assigned to the Items property of the control and is what the display-drawing code actually works from. As you see, this collection is entirely separate of the object you originally assigned to ItemsSource, and so there is no propogation of changes from one to the other. This is why the items are still in the control when you clear the original list: the control is ignoring the original list, and has its own list that contains your objects.
It's for this reason that an ItemsSource value needs to raise events - specifically INotifyCollectionChanged.NotifyCollectionChanged - so that the control knows to refresh the Items list. ObservableCollection implements this interface and raises the correct event, and so the functionality works as expected.
It's hugely important to note that this is nothing like what happens in WinForms, which is why I've been pressing you for the clarification.
EDIT: To clarify, there is no "deep copy." The code that is happening is similar in principle to the following:
private List<object> myCopy;
public void SetItemsSource(List<object> yourCopy)
{
myCopy = new List<object>();
foreach (var o in yourCopy)
{
myCopy.Add(o);
}
}
Once this code has run, there's only one copy of every item in your list. But each of the items is in both of the lists. If you change, clear or otherwise manipulate yourCopy, myCopy knows nothing about it. You cannot "destroy" any of the objects that are within the list my clearing yourCopy - all you do is release your own reference to them.
Assuming you are using WPF:
List<User> doesn't fire any event that the UI will recognise to refresh itself. If you use ObservableCollection<User> instead, your code will work.
The key difference is that ObservableCollection implements INotifyCollectionChanged, which allows the UI to recognise that the content of the collection has changed, and thus refresh the content of the ComboBox.
(Note that this does not work in WinForms. In WinForms you can set the DataSource property of the control, but the same ObservableCollection trick does not work here.)
When you set a collection reference to ItemsControl, all the combo gets is a reference, that it knows is enumerable.
It will enumerate the reference and display the items. Whether it does a deep copy or shallow copy is irrelevant, all it has is a reference (memory address effectively).
If you change your collection in some way, the combo has no way of knowing unless you tell it somehow. The reference (address) hasn't changed, everything looks the same to the combo. You seem to be thinking that the object is somehow "live" and the combo can watch the memory changing or something? This isn't the case. All it has is a reference that it can enumerate over. The contents can change but without some trigger the combo doesn't know that, and so will sit doing nothing.
ObservableCollection is designed to overcome this. It implements INotifyCollectionChanged that fires events when it changes, so the Combo knows that it must update its display.
MVVM pattern is implemented in my Silverlight4 application.
Originally, I worked with ObservableCollection of objects in my ViewModel:
public class SquadViewModel : ViewModelBase<ISquadModel>
{
public SquadViewModel(...) : base(...)
{
SquadPlayers = new ObservableCollection<SquadPlayerViewModel>();
...
_model.DataReceivedEvent += _model_DataReceivedEvent;
_model.RequestData(...);
}
private void _model_DataReceivedEvent(ObservableCollection<TeamPlayerData> allReadyPlayers, ...)
{
foreach (TeamPlayerData tpd in allReadyPlayers)
{
SquadPlayerViewModel sp = new SquadPlayerViewModel(...);
SquadPlayers.Add(sp);
}
}
...
}
Here is a peacie of XAML code for grid displaying:
xmlns:DataControls="clr-namespace:System.Windows.Controls;
assembly=System.Windows.Controls.Data"
...
<DataControls:DataGrid ItemsSource="{Binding SquadPlayers}">
...</DataControls:DataGrid>
and my ViewModel is bound to DataContext property of the view.
This collection (SquadPlayers) is not changed after its creation so I would like to change its type to
List<SquadPlayerViewModel>
. When I did that, I also added
RaisePropertyChanged("SquadPlayers")
in the end of '_model_DataReceivedEvent' method (to notify the grid that list data are changed.
The problem is that on initial displaying grid doesn't show any record... Only when I click on any column header it will do 'sorting' and display all items from the list...
Question1: Why datagrid doesn't contain items initially?
Q2: How to make them displayed automatically?
Thanks.
P.S. Here is a declaration of the new List object in my view-model:
public List<SquadPlayerViewModel> SquadPlayers { get; set; }
You can't use List as a binding source, because List not implement INotifyCollectionChanged it is require for WPF/Silverlight to have knowledge for whether the content of collection is change or not. WPF/Sivlerlight than can take further action.
I don't know why you need List<> on your view model, but If for abstraction reason you can use IList<> instead. but make sure you put instance of ObservableCollection<> on it, not the List<>. No matter what Type you used in your ViewModel Binding Only care about runtime type.
so your code should like this:
//Your declaration
public IList<SquadPlayerViewModel> SquadPlayers { get; set; }
//in your implementation for WPF/Silverlight you should do
SquadPlayers = new ObservableCollection<SquadPlayerViewModel>();
//but for other reason (for non WPF binding) you can do
SquadPlayers = new List<SquadPlayerViewModel>();
I usually used this approach to abstract my "Proxied" Domain Model that returned by NHibernate.
You'll need to have your SquadPlayers List defined something like this:
private ObservableCollection<SquadPlayerViewModel> _SquadPlayers;
public ObservableCollection<SquadPlayerViewModel> SquadPlayers
{
get
{
return _SquadPlayers;
}
set
{
if (_SquadPlayers== value)
{
return;
}
_SquadPlayers= value;
// Update bindings, no broadcast
RaisePropertyChanged("SquadPlayers");
}
}
The problem is that whilst the PropertyChanged event informs the binding of a "change" the value hasn't actually changed, the collection object is still the same object. Some controls save themselves some percieved unnecessary work if they believe the value hasn't really changed.
Try creating a new instance of the ObservableCollection and assigning to the property. In that case the currently assigned object will differ from the new one you create when data is available.