My target is to create a combobox which displays any list of strings when opened (so the standard behavior), but when the user selects one of the strings, it gets added at the top of the list under a "Recently used" separator.
Essentially, I want a control that behaves exactly like the one to select fonts in MS Word:
My start was to create a custom control with an additional Dependency property which holds the recently selected items. This list gets updated when the user selects an item from the list. I don't want to modify the original list of items, since I aim to get a reusable control where the user does not have to manage the most recent items themselves.
private static readonly DependencyPropertyKey LastSelectedItemsPropertyKey =
DependencyProperty.RegisterReadOnly(
"LastSelectedItems",
typeof (Dictionary<string, int>),
typeof (MemoryCombobox),
new FrameworkPropertyMetadata(default(ObservableCollection<string>), FrameworkPropertyMetadataOptions.None));
public static readonly DependencyProperty LastSelectedItemsProperty = LastSelectedItemsPropertyKey.DependencyProperty;
My question now is: how can I display all items (labels and both lists) in a single dropdown of the combobox, like this:
---------------------
Label: Recently Selected
---------------------
<All items from the 'LastSelectedItems' DependencyProperty>
---------------------
Label: All Items
---------------------
<All items from the 'ItemsSource' property of the combobox
---------------------
I don't want to use grouping for this, since the items would not be repeated in the "all items" list below the recently used ones, like the user would expect them to be.
Have you tried something along these lines. It uses grouping, but does it in a special way, so that mru-items are not removed from the total list/group:
XAML:
<ComboBox Name="MyCombo" SelectionChanged="MyCombo_SelectionChanged" VerticalAlignment="Top">
<ComboBox.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" Background="DarkGray" Foreground="White" FontWeight="Bold" />
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ComboBox.GroupStyle>
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" Margin="0,0,5,0" />
<TextBlock Text="{Binding Value}" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
Code Behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
m_preventFeedback = true;
ItemsList = new ObservableCollection<VMItem>
{
new VMItem(new Item("John", 1234), 2),
new VMItem(new Item("Peter", 2345), 2),
new VMItem(new Item("Michael", 3456), 2),
};
ListCollectionView view = new ListCollectionView(ItemsList);
view.GroupDescriptions.Add(new PropertyGroupDescription("CategoryId", new ItemGroupValueConverter()));
MyCombo.ItemsSource = view;
m_preventFeedback = false;
}
private ObservableCollection<VMItem> ItemsList = new ObservableCollection<VMItem>();
bool m_preventFeedback = false;
private void MyCombo_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (m_preventFeedback) return;
if (MyCombo.SelectedItem is VMItem item)
{
m_preventFeedback = true;
VMItem mru = ItemsList.FirstOrDefault(i => i.Name == item.Name && i.CategoryId == 1) ?? new VMItem(item.Item, 1);
ItemsList.Remove(mru);
ItemsList.Insert(0, mru);
MyCombo.SelectedItem = mru;
m_preventFeedback = false;
}
}
}
public class ItemGroupValueConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
switch ((int)value)
{
case 1: return "Last Used";
case 2: return "Available Items";
}
return "N/A";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return value;
}
}
public class VMItem : INotifyPropertyChanged
{
private Item m_item;
public VMItem(Item item, int categoryId)
{
m_item = item;
m_categoryId = categoryId;
}
public string Name
{
get { return m_item.Name; }
set
{
m_item.Name = value;
OnPropertyChanged("Name");
}
}
public int Value
{
get { return m_item.Value; }
set
{
m_item.Value = value;
OnPropertyChanged("Value");
}
}
private int m_categoryId;
public int CategoryId
{
get { return m_categoryId; }
set
{
m_categoryId = value;
OnPropertyChanged("CategoryId");
}
}
public Item Item => m_item;
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string property)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
}
public class Item
{
public Item(string name, int value)
{
Name = name;
Value = value;
}
public string Name { get; set; }
public int Value { get; set; }
}
Related
I'm working on a chat,I have listbox, where I show users. I also have UserViewModel with some fields.
public class UserViewModel
{
public int Id { get; set; }
public string Username { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
I want to add objects to listbox in case if their Firstname&&Lastname are not null, show their Name, otherwise show their Username. I also need to get their id after selecting any item in listbox. So, after loading the page, I got user list from database, and trying to add to listbox using foreach loop.
Here is xaml code for adding items in listbox.
<ListBox
Name="usersListBox"
Grid.Row="0"
Grid.Column="0"
Background="LightGreen"
Margin="10"
FontSize="15"
FontWeight="Medium"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.CanContentScroll="True"
Loaded="ListBox_Loaded"
ItemSource = {Binding UsersList}
SelectedItem="{Binding SelectedUser}>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text = "{Binding Path Username}"/>
</StackPanel>
</DataTemplate>
I tried to do the same thing with c# code(So, I commented the whole <ListBox.Itemtemplate> tag, removed ItemSource to add it with c#).
namespace Chat.PresentationLayer.Pages
{
/// <summary>
/// Interaction logic for Chat.xaml
/// </summary>
public partial class Chat : Page
{
private readonly UserManager userManager = new UserManager();
public List<UserViewModel> UsersList { get; set; } = new List<UserViewModel>();
public UserViewModel SelectedUser { get; set; } = new UserViewModel();
static bool b = true;
public Chat()
{
InitializeComponent();
}
private void ListBox_Loaded(object sender, RoutedEventArgs e)
{
var users = userManager.GetAllUsers();
//usersListBox.SelectionChanged += OnItemSelect;
usersListBox.ItemsSource = users;
foreach (var user in users)
{
UsersList.Add(new UserViewModel { Id = user.Id, Username = user.Username, FirstName = user.FirstName, LastName = user.LastName });
StackPanel sp = new StackPanel();
TextBox tb = new TextBox();
Binding myBinding = new Binding();
myBinding.Source = user;
myBinding.Path = new PropertyPath("Username");
myBinding.Mode = BindingMode.TwoWay;
myBinding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
BindingOperations.SetBinding(tb, TextBox.TextProperty, myBinding);
sp.Children.Add(tb);
DataTemplate dt = new DataTemplate() { DataType = sp };
usersListBox.ItemTemplate = dt;
//The code below in comments represents exactly
// what I want despite getting user Id after selecting.
//usersListBox.Items.Add(user);
//if (string.IsNullOrEmpty(user.FirstName))
//{
// if (user.Username == SessionInfo.CurrentUserInfo.Username)
// {
// usersListBox.Items.Add(user.Username + " (You)");
// continue;
// }
// usersListBox.Items.Add(user.Username);
// //usersListBox.ItemsSource = UsersList
//}
//else
//{
// if (user.Username == SessionInfo.CurrentUserInfo.Username)
// {
// usersListBox.Items.Add(user.FirstName + " " + user.LastName + " (You)");
// continue;
// }
// usersListBox.Items.Add(user.FirstName + " " + user.LastName);
//}
}
}
Note: The Binding part I added recently, before it the code was in this way:
tb.Text = user.Username //it is pretty normal for this not to work, I know
This code actually doesn't work.
In ListBox there are Chat.BusinessLogicLayer.Models.UserViewModel instead of the name.
Result is the same without the whole foreach loop(which is pretty logical)
If I remove ItemSourse line, it won't add anything in listbox(in xaml it worked correctly without ItemSource property too, I don't know exactly why).
Thanks for any help!
Please, edit question title, I could not set anything better)
You should use XAML only. Just make UserList a ObservableCollection<UserViewModel> so that adding items to it will trigger the ListBox to update the content. This way you don't need any foreach anymore. Remove the initialization of SelectedUser. Create a view model for the (each) Page. Move all the page data initialization logic to the view model. Move model interaction (get users from database) to the view model. Add a IValueConverter to the TextBlock binding to format the displayed username.
PageViewModel.cs
class ChatPageViewModel : INotifyPropertyChanged
{
public ChatPageViewModel()
{
this.Users = new ObservableCollection<UserViewModel>();
}
private void OnSelectedUserChanged(UserViewModel selectedUser)
{
// TODO::Handle selected user
}
private bool CanInitializePage(object commandParameter)
{
return true;
}
private void InitializePage(object commandParameter)
{
this.Users = new ObservableCollection<UserViewModel>(userManager.GetAllUsers());
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public ICommand InitializePageCommand => new RelayCommand(InitializePage, CanInitializePage);
private UserViewModel selectedUser;
public UserViewModel SelectedUser
{
get => this.selectedUser;
set
{
this.selectedUser = value;
OnSelectedUserChanged();
OnPropertyChanged();
}
}
private ObservableCollection<UserViewModel> users;
public ObservableCollection<UserViewModel> Users
{
get => this.users;
set
{
this.users = value;
OnPropertyChanged();
}
}
private UserManager userManager { get; } = new UserManager();
}
UsernameFormatConverter.cs (to format the username)
[ValueConversion(typeof(UserViewModel), typeof(string))]
public class UsernameFormatConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value is UserViewModel user)
{
return string.IsNullOrEmpty(user.FirstName) || string.IsNullOrEmpty(user.FirstName)
? user.Username
: user.FirstName + " " + user.LastName + " (You)";
}
return Binding.DoNothing;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
ChatPage.xaml
<Page x:Class="ChatPage">
<Page.DataContext>
<local:ChatPageVieModel />
</Page.DataContext>
<Page.Resources>
<local:UsernameFormatConverter x:Key="UsernameFormatConverter" />
</Page.Resources>
<Grid>
<ListBox Name="usersListBox"
ItemsSource="{Binding Users}"
SelectedItem="{Binding SelectedUser, Mode=TwoWay}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding ., Converter={StaticResource UsernameFormatConverter}}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</Grid>
</Page>
ChatPage.xaml.cs
public partial class Chat : Page
{
public Chat()
{
this.Loaded += OnPageLoaded;
}
// I recommend to move page initialization to the code location where you load pages instead of doing it in the Loaded event handler
private void OnPageLoaded(object sender, RoutedEventArgs e)
{
if (this.DataContext is ChatPageViewModel viewModel
&& viewModel.InitializePageCommand.canExecute(null))
{
viewModel.InitializePageCommand.Execute(null);
}
}
}
}
I'm just getting used to MVVM and want to do without the code-behind and define everything in the view-models.
the combobox represents several selection options (works). I would like to query the elements that have been checked.
Unfortunately I can't access them. The textbox should display all selected elements as concatenated string.
View-Model
class MainViewModel : BaseViewModel
{
#region Fields
private ObservableCollection<EssayTypeViewModel> _essayTypes;
private EssayTypeViewModel _selectedEssayTypes;
#endregion
public ObservableCollection<EssayTypeViewModel> EssayTypes
{
get => _essayTypes;
set
{
if (_essayTypes == value) return;
_essayTypes = value; OnPropertyChanged("EssayTypes");
}
}
public EssayTypeViewModel SelectedEssayTypes
{
get => _selectedEssayTypes;
set { _selectedEssayTypes = value; OnPropertyChanged("SelectedEssayTypes"); }
}
public MainViewModel()
{
// Load Essay Types
EssayTypeRepository essayTypeRepository = new EssayTypeRepository();
var essayTypes = essayTypeRepository.GetEssayTypes();
var essayTypeViewModels = essayTypes.Select(m => new EssayTypeViewModel()
{
Text = m.Text
});
EssayTypes = new ObservableCollection<EssayTypeViewModel>(essayTypeViewModels);
}
}
XAML
<ListBox x:Name="Listitems" SelectionMode="Multiple" Height="75" Width="200" ItemsSource="{Binding EssayTypes}" >
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding Text}" IsChecked="{Binding Checked}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<TextBox Text="{Binding Path=SelectedEssayTypes}" Grid.Column="0" Width="160" Height="25" Margin="0,140,0,0"/>
You could hook up an event handler to the PropertyChanged event of all EssayTypeViewModel objects in the EssayTypes collection and raise the PropertyChanged event for a read-only property of the MainViewModel that returns all selected elements as concatenated string:
public MainViewModel()
{
// Load Essay Types
EssayTypeRepository essayTypeRepository = new EssayTypeRepository();
var essayTypes = essayTypeRepository.GetEssayTypes();
var essayTypeViewModels = essayTypes.Select(m =>
{
var vm = EssayTypeViewModel()
{
Text = m.Text
};
vm.PropertyChanged += OnPropertyChanged;
return vm;
});
EssayTypes = new ObservableCollection<EssayTypeViewModel>(essayTypeViewModels);
}
private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "Checked")
OnPropertyChanged("SelectedItems");
}
public string SelectedItems => string.Join(",", EssayTypes.Where(x => x.Checked).ToArray());
This requires the EssayTypeViewModel class to implement the INotifyPropertyChanged interface (by for example deriving from your BaseViewModel class).
You can apply Mode = Two way on the checkbox binding.
<CheckBox Content="{Binding Text}" IsChecked="{Binding Checked, Mode=TwoWay}"/>
then you can iterate through the essay types collection to check if the item entry was checked.
For ex. Sample code can be:
foreach (var essayTypeInstance in EssayTypes)
{
if(essayTypeInstance.Checked)
{
// this value is selected
}
}
Hope this helps.
mm8 answer works. In the meantime i came up with another approach. Not 100% MVVM compatible but it works and is quite simple.
XAML
<ListBox x:Name="ListItems" SelectionMode="Multiple" Height="75" Width="200" ItemsSource="{Binding CollectionOfItems}" >
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding Name}" IsChecked="{Binding Checked, Mode=TwoWay}" Unchecked="GetCheckedElements" Checked="GetCheckedElements" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<TextBox Text="{Binding SelectedItemsString, UpdateSourceTrigger=PropertyChanged}" Grid.Column="0" Width="160" Height="25" Margin="0,140,0,0"/>
Code Behind
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel();
}
private void GetCheckedElements(object sender, RoutedEventArgs e)
{
(DataContext as MainViewModel)?.FindCheckedItems();
(DataContext as MainViewModel)?.ConcatSelectedElements();
}
}
Model
public class Items
{
public bool Checked { get; set; }
public string Name { get; set; }
}
ItemsViewModel (BaseViewModel only implements INotifyPropertyChanged)
class ItemsViewModel : BaseViewModel
{
private bool _checked;
private string _name;
public bool Checked
{
get => _checked;
set
{
if (value == _checked) return;
_checked = value;
OnPropertyChanged("Checked");
}
}
public string Name
{
get => _name;
set
{
if (value == _name) return;
_name = value;
OnPropertyChanged("Name");
}
}
}
MainViewModel
public class MainViewModel : BaseViewModel
{
private string _selectedItemsString;
private ObservableCollection<Items> _selectedItems;
public ObservableCollection<Items> CollectionOfItems { get; set; }
public ObservableCollection<Items> SelectedItems
{
get => _selectedItems;
set
{
_selectedItems = value;
OnPropertyChanged("SelectedItems");
}
}
public string SelectedItemsString
{
get => _selectedItemsString;
set
{
if (value == _selectedItemsString) return;
_selectedItemsString = value;
OnPropertyChanged("SelectedItemsString");
}
}
public MainViewModel()
{
CollectionOfItems = new ObservableCollection<Items>();
SelectedItems = new ObservableCollection<Items>();
CollectionOfItems.Add(new Items { Checked = false, Name = "Item 1" });
CollectionOfItems.Add(new Items { Checked = false, Name = "Item 2" });
CollectionOfItems.Add(new Items { Checked = false, Name = "Item 3" });
}
public void FindCheckedItems()
{
CollectionOfItems.Where(x => x.Checked).ToList().ForEach(y => SelectedItems.Add(y));
}
public void ConcatSelectedElements()
{
SelectedItemsString = string.Join(", ", CollectionOfItems.Where(x => x.Checked).ToList().Select(x => x.Name)).Trim();
}
}
GitHub Link: https://github.com/babakin34/wpf_test/tree/master/WpfApp1
I have the following classes:
VIEWMODELS:
public class PersonVM : BindableBase
{
public int ID { get; set; }
private string _lastName;
public string LastName
{
get { return _lastName; }
set { SetProperty(ref _lastName, value); }
}
}
public class MainVM : BindableBase
{
public ObservableCollection<PersonVM> People { get; set; }
private PersonVM _selectedPerson;
public PersonVM SelectedPerson
{
get { return _selectedPerson; }
set { SetProperty(ref _selectedPerson, value); }
}
public MainVM()
{
People = new ObservableCollection<PersonVM>()
{
new PersonVM()
{
ID = 1,
LastName = "AA"
},
new PersonVM()
{
ID = 2,
LastName = "BB"
},
};
SelectedPerson = People.First();
}
}
VIEW:
<Grid>
<StackPanel>
<ComboBox ItemsSource="{Binding People}"
SelectedItem="{Binding SelectedPerson}"
DisplayMemberPath="LastName"
Margin="0,5,0,25"/>
<DataGrid ItemsSource="{Binding People}"/>
</StackPanel>
</Grid>
How can I achieve that the "MainVM.SelectedPerson" from ComboBox is notified when user selects the empty element, which is caused by the Datagrid's default last entry?
PS: I am using Prism, but the problem is not Prism related. You can replace BindableBase by INotifyPropertyChanged.
Edit:
The real issue here is a bit different than i thought at first. When selecting the insert row, you see in the output window that WPF is unable to cast a "NamedObject" into a "PersonVM". The Datagrid creates a NamedObject for the insert row to work with, but the binding simply does not work since it's of the wrong type.
The clean and easy solution is to create a converter to check if the object is of type PersonVM, otherwise return null (and not the NamedObject instance).
public class MyConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is PersonVM)
return value;
return null;
}
}
And in the xaml
<DataGrid x:Name="DataGrid"
ItemsSource="{Binding People}"
SelectedCellsChanged="DataGrid_OnSelectedCellsChanged"
SelectedItem="{Binding SelectedPerson,
Converter={StaticResource myConverter}}"
Old and dirty
Not the nicest way, but if you don't mind using the viewmodel (or abstracting it via an interface) in the codebehind you can use the "SelectionChanged" event to set the SelectedPerson in the ViewModel to null if any of the selected items are not of the type you need.
private void Selector_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
bool invalidStuffSelected = false;
//throw new System.NotImplementedException();
foreach (var obj in DataGrid.SelectedItems)
{
if (!(obj is PersonVM))
invalidStuffSelected = true;
}
MainVM vm = (MainVM) this.DataContext;
if (invalidStuffSelected)
vm.SelectedPerson = null;
}
In your example the selectionmode "single" would make more sense since the combobox can only show one selected value.
I have this Order class and I am using MVVM:
namespace drinksMVVM.Models
{
public class Order : INotifyPropertyChanged
{
private Item _OrderItem;
private ObservableCollection<Item> _Supplements;
private double _OrderPrice;
public Item OrderItem
{
get
{
return _OrderItem;
}
set
{
_OrderItem = value;
OnPropertyChanged("OrderItem");
_OrderPrice += _OrderItem.Price;
}
}
public ObservableCollection<Item> Supplements
{
get
{
return _Supplements;
}
set
{
_Supplements = value;
OnPropertyChanged("Supplements");
foreach(var supplement in _Supplements)
{
_OrderPrice += supplement.Price;
}
}
}
public double OrderPrice
{
get
{
return _OrderPrice;
}
set
{
_OrderPrice = value;
OnPropertyChanged("Price");
}
}
public Order()
{
}
public Order(Item item, ObservableCollection<Item> items)
{
_OrderItem = item;
_Supplements = items;
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
}
It contains object from type Item(properties Name and Price) and ObservableCollection of type Item. The Order object certainly contains one Item which is a single food/drink as order, but can contain Item and ObservableCollection, which is a food/drink with additional supplements.
Simple example:
I have order of Coffee with Sugar and Cream and I want to display them something like that:
Coffee (OrderItem)
Sugar+Cream (Supplements)
1,50 (OrderPrice)
The layout does not matter in this case
My question here is how to display the data in my view. I have two ideas which don't know how to continue.
DataTemplate with textblocks, each textblock is bind to a property of the Order class, but don't know how to display every single item name from the ObservableCollection.
<DataTemplate x:Key="OrderTemplate">
<StackPanel Orientation="Vertical">
<TextBlock x:Name="OrderName" Text="{Binding OrderItem.Name}"/>
<TextBlock x:Name="OrderSupplements"
Text="{Binding Supplements[0].Name}"/> ///Can't solve this
binding
<TextBlock x:Name="OrderPrce" Text="{Binding OrderPrice}"/>
</StackPanel>
</DataTemplate>
2.Override the ToString() method of the Order class in order to display all of the data in one single textblock, but also can't find a solution how to display all supplements names.
public override string ToString()
{
return OrderItem.Name + " " +/*every single supplement name*/+" "
+OrderPrice.ToString();
}
And
<DataTemplate x:Key="OrderTemplate1">
<StackPanel Orientation="Vertical">
<TextBlock x:Name="test" Text="{Binding}"/>
</StackPanel>
</DataTemplate>
As far as option #2 goes...
public override string ToString()
{
string orderStr = string.Empty;
orderStr += _OrderItem.Name + " ";
foreach(Item supplement in Supplements)
{
orderStr += supplement.Name + " ";
}
orderStr += OrderPrice.ToString();
return orderStr;
}
I managed to solve option #1 with IValueConverter
[ValueConversion(typeof(List<Item>),typeof(string))]
public class MyConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
List<Item> items = (List<Item>)value;
string supplementsString = string.Empty;
if (items.Count >= 1)
{
foreach (var item in items)
{
supplementsString += item.Name + " " + item.Price.ToString() + " " + Environment.NewLine;
}
}
return supplementsString;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
And XAML part
<converters:MyConverter x:Key="MyConverterTest"/>
<TextBlock x:Name="OrderSupplements"
Text="{Binding Supplements, Converter={StaticResource MyConverter}}"/>
I am just starting with MVVM and have hit a hurdle that I hope someone can help me with. I am trying to create a simple View with 2 listboxes. A selection from the first listbox will populate the second list box. I have a class created that stores the information I want to bind to.
MyObject Class (Observable Object is just a base class that implements INotifyPopertyChanged)
public class MyObject : ObservableObject
{
String _name = String.Empty;
ObservableCollection<MyObject> _subcategories;
public ObservableCollection<MyObject> SubCategories
{
get { return _subcategories; }
set
{
_subcategories = value;
RaisePropertyChanged("SubCategories");
}
}
public String Name
{
get { return _name; }
set
{
_name = value;
RaisePropertyChanged("Name");
}
}
public MyObject()
{
_subcategories = new ObservableCollection<EMSMenuItem>();
}
}
In my viewmodel I have two ObservableCollections created
public ObservableCollection<EMSMenuItem> Level1MenuItems { get; set; }
public ObservableCollection<EMSMenuItem> Level2MenuItems { get; set; }
In my constructor of the ViewModel I have:
this.Level1MenuItems = new ObservableCollection<EMSMenuItem>();
this.Level2MenuItems = new ObservableCollection<EMSMenuItem>();
this.Level1MenuItems = LoadEMSMenuItems("Sample.Xml");
That works fine for the Level1 items and they correctly show in the View. However I have a command that gets called when the user clicks an item in the listbox, which has the following:
Level2MenuItems = ClickedItem.SubCategories;
For some reason this does not update the UI of the second listbox. If I put a breakpoint at this location I can see that Level2MenuItems has the correct information stored in it. If I write a foreach loop and add them individually to the Level2MenuItems collection then it does display correctly.
Also as a test I added the following to the constructor:
Level2MenuItems = Level1MenuItems[0].SubCategories;
And that updated correctly.
So why would the code work as expected in the constructor, or when looping through, but not when a user clicks on an item in the listbox?
You need to raise the change notification on the Level2MenuItems property.
Instead of having
public ObservableCollection<EMSMenuItem> Level2MenuItems { get; set; }
you need
private ObservableCollection<EMSMenuItem> _level2MenuItems;
public ObservableCollection<EMSMenuItem> Level2MenuItems
{
get { return _level2MenuItems; }
set
{
_level2MenuItems = value;
RaisePropertyChanged(nameof(Level2MenuItems));
}
}
The reason the former works in the constructor is that the Binding has not taken place yet. However since you are changing the reference via a command execute which happens after the binding you need to tell view that it changed
You need to make your poco class within the ObservableCollection implement INotifyPropertyChanged.
Example:
<viewModels:LocationsViewModel x:Key="viewModel" />
.
.
.
<ListView
DataContext="{StaticResource viewModel}"
ItemsSource="{Binding Locations}"
IsItemClickEnabled="True"
ItemClick="GroupSection_ItemClick"
ContinuumNavigationTransitionInfo.ExitElementContainer="True">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" Margin="0,0,10,0" Style="{ThemeResource ListViewItemTextBlockStyle}" />
<TextBlock Text="{Binding Latitude, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Style="{ThemeResource ListViewItemTextBlockStyle}" Margin="0,0,5,0"/>
<TextBlock Text="{Binding Longitude, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Style="{ThemeResource ListViewItemTextBlockStyle}" Margin="5,0,0,0" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
public class LocationViewModel : BaseViewModel
{
ObservableCollection<Location> _locations = new ObservableCollection<Location>();
public ObservableCollection<Location> Locations
{
get
{
return _locations;
}
set
{
if (_locations != value)
{
_locations = value;
OnNotifyPropertyChanged();
}
}
}
}
public class Location : BaseViewModel
{
int _locationId = 0;
public int LocationId
{
get
{
return _locationId;
}
set
{
if (_locationId != value)
{
_locationId = value;
OnNotifyPropertyChanged();
}
}
}
string _name = null;
public string Name
{
get
{
return _name;
}
set
{
if (_name != value)
{
_name = value;
OnNotifyPropertyChanged();
}
}
}
float _latitude = 0;
public float Latitude
{
get
{
return _latitude;
}
set
{
if (_latitude != value)
{
_latitude = value;
OnNotifyPropertyChanged();
}
}
}
float _longitude = 0;
public float Longitude
{
get
{
return _longitude;
}
set
{
if (_longitude != value)
{
_longitude = value;
OnNotifyPropertyChanged();
}
}
}
}
public class BaseViewModel : INotifyPropertyChanged
{
#region Events
public event PropertyChangedEventHandler PropertyChanged;
#endregion
protected void OnNotifyPropertyChanged([CallerMemberName] string memberName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(memberName));
}
}
}
Your Subcategories property should be read-only.