I'm trying to bind an ObservableCollection of custom objects called ItemGroup which itself contains an ObservableCollection of Item objects (among other Properties) to a LongListSelector (to create a grouping of Item objects) in WP8.1 Silverlight:
public class Item : INotifyPropertyChanged {
private string _name = "";
public string Name {
get {
return _name;
}
set {
if (value != _name) {
_name = value;
NotifyPropertyChanged("Name");
}
}
}
private int _cost = 0;
public int Cost {
get {
return _cost;
}
set {
if (value != _cost) {
_cost = value;
NotifyPropertyChanged("Cost");
}
}
}
}
public class ItemGroup : INotifyPropertyChanged {
private string _groupName = "";
public string GroupName {
get {
return _groupName;
}
set {
if (value != _groupName) {
_groupName = value;
NotifyPropertyChanged("GroupName");
}
}
}
private int _totalCost = 0;
public int TotalCost {
get {
return _totalCost;
}
set {
if (value != _totalCost) {
_totalCost = value;
NotifyPropertyChanged("TotalCost");
}
}
}
private ObservableCollection<Item> _items = new ObservableCollection<Item>();
public ObservableCollection<Item> Items {
get {
return _items;
}
set {
if (value != _items) {
_items = value;
NotifyPropertyChanged("Items");
}
}
}
}
ViewModel contains the property:
private ObservableCollection<ItemGroup> _itemGroupList = new ObservableCollection<ItemGroup>();
public ObservableCollection<ItemGroup> ItemGroupList {
get {
return _itemGroupList;
}
set {
if (value != _itemGroupList) {
_itemGroupList = value;
NotifyPropertyChanged("ItemGroupList");
}
}
}
And the XAML:
<phone:LongListSelector ItemsSource="{Binding ItemGroupList}">
<!-- Template for ItemGroup Items -->
<phone:LongListSelector.ItemTemplate>
<DataTemplate>
<Grid Height="Auto" Margin="12,0,2,15">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Item Name -->
<TextBlock Text="{Binding Items.Name}" Grid.Column="0"/>
<!-- Item Cost -->
<TextBlock Text="{Binding Items.Cost, StringFormat='{}{0} dollars'}" Grid.Column="1"/>
</Grid>
</DataTemplate>
</phone:LongListSelector.ItemTemplate>
<!-- Template for ItemGroup headers -->
<phone:LongListSelector.GroupHeaderTemplate>
<DataTemplate>
<Grid Background="#FFA2A2A2" Margin="0,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- ItemGroup Group Name -->
<TextBlock Grid.Column="0" Text="{Binding GroupName}"/>
<!-- ItemGroup Total Cost -->
<TextBlock Grid.Column="1" Text="{Binding TotalCost}"/>
</Grid>
</DataTemplate>
<phone:LongListSelector.GroupHeaderTemplate>
</phone:LongListSelector>
Which isn't working. The direct Properties of ItemGroup (GroupName and TotalCost) bind correctly and are visible, but no Item objects are shown! I suspect this is because I am passing to ItemTemplate the wrong source? I have tried binding the Collection directly in the LLS by ItemSource={Binding ItemGroupList.Items}, but that too doesn't work.
Update: Binding a Header control to Items.Count yields the actual number of Item objects inside Items of ItemGroup, which indicates that there indeed exists a list of Item objects for each ItemGroup.
Note that you've got a nested collection there, so you will need 2 items controls. You can't bind to "Items.Name" or "Items.Cost" because those are not properties. You could use "Items[0].Name", but I guess that you would probably want to list all the items, something like this (inside "LongListSelector.ItemTemplate"):
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Item Name -->
<TextBlock Text="{Binding Items.Name}" Grid.Column="0"/>
<!-- Item Cost -->
<TextBlock Text="{Binding Items.Cost, StringFormat='{}{0} dollars'}" Grid.Column="1"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Related
I have make a simple project to investigate on a performance issue in my Xamarin.Forms application.
I have two nested list for a classic master-detail structure. Accord to the MVVM pattern, I implemented the INotifyPropertyChanged interface for ViewModel classes and use ObservableCollection for lists.
In the main view I have two standard ListView, the first (masters) binded to the master collection, che second (details) binded to the details collection of the selected item of the master list. Both have CachingStrategy="RecycleElement".
When run UWP application, in Diagnostic Tools of Visual Studio can be see memory usage of application.
When an element of the masters list is selected, the details list is rendered and the used memory grow up, when masters list selected item change, the details list is redrowed with the respective items and used memory grow up, also if there is less items to display, if previous masters item is selected again, used memory grow up again. Every time that masters list selected items change and the details list is refreshed, used memory grow up without ever decreasing. Seems that objects used for render the details list items never be collected by GC.
I think it's too evident effect to be a bug no one has ever noticed, but the test project is very minimal, all viewmodels inherits from MvvmLight ViewModelBase class, all properties use Set method and all list properties type is ObservableCollection, the only custom code is for initial load.
I think that I make a mistake that I can't see.
Below viewmodels and view code
MainViewModel.cs
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
this.Load();
}
private RelayCommand _reloadCommand;
public RelayCommand ReloadCommand { get { return _reloadCommand ?? (_reloadCommand = new RelayCommand(this.Load)); } }
private ObservableCollection<ItemViewModel> _items;
public ObservableCollection<ItemViewModel> Items { get { return _items; } set { this.Set(ref _items, value); } }
private ItemViewModel _selectedItem;
public ItemViewModel SelectedItem { get { return _selectedItem; } set { this.Set(ref _selectedItem, value); } }
private SubitemViewModel _selectedSubitem;
public SubitemViewModel SelectedSubitem { get { return _selectedSubitem; } set { this.Set(ref _selectedSubitem, value); } }
private void Load()
{
int[] ranges = { 15, 10, 5, 20 };
this.Items = new ObservableCollection<ItemViewModel>(
from i in Enumerable.Range(1, 15)
select new ItemViewModel()
{
Label = $"Item {i}",
Items = new ObservableCollection<SubitemViewModel>(from s in Enumerable.Range(1, ranges[i % 4] )
select new SubitemViewModel() { Label = $"Subitem {i}.{s}" })
});
}
}
ItemViewModel.cs
public class ItemViewModel : ViewModelBase
{
private string _label;
public string Label { get { return _label; } set { this.Set(ref _label, value); } }
private ObservableCollection<SubitemViewModel> _items;
public ObservableCollection<SubitemViewModel> Items { get { return _items; } set { this.Set(ref _items, value); } }
}
SubItemViewModel.cs
public class SubitemViewModel : ViewModelBase
{
private string _label;
public string Label { get { return _label; } set { this.Set(ref _label, value); } }
}
MainView.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:vm="clr-namespace:App1.ViewModel"
x:Class="App1.MainPage">
<StackLayout>
<Grid Padding="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="3*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="*" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<Label Grid.Row="0" Grid.Column="0" Text="Masters" />
<Label Grid.Row="0" Grid.Column="1" Text="Details" />
<ListView Grid.Column="0" Grid.Row="1"
ItemsSource="{Binding Items}"
CachingStrategy="RecycleElement"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="vm:ItemViewModel">
<ViewCell>
<ViewCell.View>
<Label Grid.Row="0" Grid.Column="1" Text="{Binding Label}" />
</ViewCell.View>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<ListView Grid.Column="1" Grid.Row="1"
ItemsSource="{Binding SelectedItem.Items}"
SelectedItem="{Binding SelectedSubitem, Mode=TwoWay}"
CachingStrategy="RecycleElement">
<ListView.ItemTemplate>
<DataTemplate x:DataType="vm:SubitemViewModel">
<ViewCell>
<ViewCell.View>
<Label Grid.Row="0" Grid.Column="1" Text="{Binding Label}" />
</ViewCell.View>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</StackLayout>
</ContentPage>
The complete test project can be download here: MemoryTest.zip
I've been digging back into some projects in WPF, and come across a hurdle that I haven't been able to find a directly related solution for.
Essentially I want to filter a child property of a SelectedItem dynamically (via text entered in the filter box, something along the lines of .Contains(filter)). The UI displays correctly in the sample project, but after attempting to implement solutions from every hit possible on SO or otherwise, I've come up blank, or making serious compromises to the MVVM pattern.
ParentItem:
public class ParentItem
{
public string Name { get; set; }
public List<string> ChildItems { get; set; }
public DateTime CreatedOn { get; set; }
public bool IsActive { get; set; }
public ParentItemStatus Status { get; set; }
}
public enum ParentItemStatus
{
Status_One,
Status_Two
}
ViewModel:
public class MainWindowViewModel : ViewModelBase
{
public ObservableCollection<ParentItem> ParentItems { get; set; }
public MainWindowViewModel()
{
ParentItems = new ObservableCollection<ParentItem>();
LoadDummyParentItems();
}
private ICommand _filterChildrenCommand;
public ICommand FilterChildrenCommand => _filterChildrenCommand ?? (_filterChildrenCommand = new RelayCommand(param => FilterChildren((string)param), param => CanFilterChildren((string)param)));
private bool CanFilterChildren(string filter)
{
//TODO: Check for selected item in real life.
return filter.Length > 0;
}
private void FilterChildren(string filter)
{
//TODO: Filter?
}
private void LoadDummyParentItems()
{
for (var i = 0; i < 20; i++)
{
ParentItems.Add(new ParentItem()
{
Name = $"Parent Item {i}",
CreatedOn = DateTime.Now.AddHours(i),
IsActive = i % 2 == 0 ? true : false,
Status = i % 2 == 0 ? ParentItemStatus.Status_Two : ParentItemStatus.Status_One,
ChildItems = new List<string>() { $"Child one_{i}", $"Child two_{i}", $"Child three_{i}", $"Child four_{i}" }
});
}
}
}
MainWindow:
<Window x:Class="FilteringDemo.Views.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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:FilteringDemo.Views"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<CollectionViewSource x:Key="ChildItemsViewSource" Source="{Binding ElementName=ItemList, Path=SelectedItem.ChildItems}" />
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=".25*"/>
<ColumnDefinition Width=".75*"/>
</Grid.ColumnDefinitions>
<ListView x:Name="ItemList" Grid.Column="0" Margin="2" ItemsSource="{Binding ParentItems}" SelectionMode="Single">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="1*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding ElementName=ItemList, Path=SelectedItem.Name}" Margin="2"/>
<TextBlock Grid.Column="1" Text="{Binding ElementName=ItemList, Path=SelectedItem.CreatedOn}" Margin="2"/>
<TextBlock Grid.Column="2" Text="{Binding ElementName=ItemList, Path=SelectedItem.IsActive}" Margin="2"/>
<TextBlock Grid.Column="3" Text="{Binding ElementName=ItemList, Path=SelectedItem.Status}" Margin="2"/>
</Grid>
<ListView Grid.Row="1" Margin="2" ItemsSource="{Binding Source={StaticResource ChildItemsViewSource}}" />
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Contains:" Margin="2" VerticalAlignment="Center"/>
<TextBox x:Name="ChildFilterInput" Grid.Column="1" Margin="2" />
<Button Grid.Column="2" Content="Filter" Width="100" Margin="2" Command="{Binding FilterChildrenCommand}" CommandParameter="{Binding ElementName=ChildFilterInput, Path=Text}"/>
</Grid>
</Grid>
</Grid>
</Window>
I've tried various implementations of adding a Filter event handler on the CollectionViewSource but have been unable to make them dynamic. It also seems like most examples/tutorials only deal directly with the parent item or static filters.
In a non-MVVM mindset, I was thinking to have an interaction trigger drive the selected item back into the ViewModel, and then create a filtered ICollectionView which the ChildItems ListView would bind to, but it seems like I can't be the only person trying this, and that there must be an easier MVVM binding friendly way.
The following example shows a simple solution to implement live filtering on a collection:
Person.cs
class Person
{
public Person(string firstName, string lastName)
{
this.FirstName = firstName;
this.LastName = lastName;
}
public string FirstName { get; set; }
public string LastName { get; set; }
}
ViewModel.cs
class ViewModel
{
public ViewModel()
{
this.Persons = new ObservableCollection<Person>()
{
new Person("Derek", "Zoolander"),
new Person("Tony", "Montana"),
new Person("John", "Wick"),
new Person("The", "Dude"),
new Person("James", "Bond"),
new Person("Walter", "White")
};
}
private void FilterData(string filterPredicate)
{
// Execute live filter
CollectionViewSource.GetDefaultView(this.Persons).Filter =
item => (item as Person).FirstName.StartsWith(filterPredicate, StringComparison.OrdinalIgnoreCase);
}
private string searchPredicate;
public string SearchPredicate
{
get => this.searchFilter;
set
{
this.searchPredicate = value;
FilterData(value);
}
}
public ObservableCollection<Person> Persons { get; set; }
}
MainWindow.xaml
<Window>
<Window.DataContext>
<ViewModel />
</Window.DataContext>
<StackPanel>
<TextBox Text="{Binding SearchPredicate, UpdateSourceTrigger=PropertyChanged"} />
<ListView ItemsSource="{Binding Persons}">
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn Header="Firstname" DisplayMemberBinding="{Binding FirstName}" />
<GridViewColumn Header="Lastname" DisplayMemberBinding="{Binding LastName}" />
</GridView.Columns>
</GridView>
</ListView.View>
</ListView>
</StackPanel>
</Window>
Update
It seems like you are having problems to filter the child items. The following example is more specific to your scenario:
DataItem.cs
class DataItem
{
public DataItem(string Name)
{
this.Name = name;
}
public string Name { get; set; }
public ObservableCollection<DataItem> ChildItems { get; set; }
}
ViewModel.cs
class ViewModel
{
public ViewModel()
{
this.ParentItems = new ObservableCollection<DataItem>()
{
new DataItem("Ben Stiller") { ChildItems = new ObservableCollection<DataItem>() { new DataItem("Zoolander"), new DataItem("Tropical Thunder") }},
new DataItem("Al Pacino") { ChildItems = new ObservableCollection<DataItem>() { new DataItem("Scarface"), new DataItem("The Irishman") }},
new DataItem("Keanu Reeves") { ChildItems = new ObservableCollection<DataItem>() { new DataItem("John Wick"), new DataItem("Matrix") }},
new DataItem("Bryan Cranston") { ChildItems = new ObservableCollection<DataItem>() { new DataItem("Breaking Bad"), new DataItem("Malcolm in the Middle") }}
};
}
private void FilterData(string filterPredicate)
{
// Execute live filter
CollectionViewSource.GetDefaultView(this.SelectedParentItem.ChildItems).Filter =
item => (item as DataItem).Name.StartsWith(filterPredicate, StringComparison.OrdinalIgnoreCase);
}
private string searchPredicate;
public string SearchPredicate
{
get => this.searchFilter;
set
{
this.searchPredicate = value;
FilterData(value);
}
}
public ObservableCollection<DataItem> ParentItems { get; set; }
public DataItem SelectedParentItem { get; set; }
}
MainWindow.xaml
<Window>
<Window.DataContext>
<ViewModel />
</Window.DataContext>
<StackPanel>
<ListView ItemsSource="{Binding ParentItems}"
SelectedItem="{Binding SelectedParentItem}">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<TextBox Text="{Binding SearchPredicate, UpdateSourceTrigger=PropertyChanged}" />
<ListView ItemsSource="{Binding SelectedParentItem.ChildItems}" />
</StackPanel>
</Window>
Using the examples from #BionicCode- I added a SelectedParentItem INPC property to the ViewModel, performed the filtering on that via CollectionViewSource.Filter, and bound the ChildItems ListView to SelectedParentItem.ChildItems.
I did not bind the text box property changed to a backing field in the VM as per #BionicCode's example, as the "real" ChildItems might be in the mid 10,000's and I didn't want it to filter on each keystroke. So this answer implements the filter button and text box command, and the CanFilterChildren is doing a proper null check.
MainWindowViewModel.cs:
public class MainWindowViewModel : ViewModelBase
{
public ObservableCollection<ParentItem> ParentItems { get; set; }
private ParentItem _selectedParentItem;
public ParentItem SelectedParentItem
{
get { return _selectedParentItem; }
set { SetProperty(ref _selectedParentItem, value); }
}
public MainWindowViewModel()
{
ParentItems = new ObservableCollection<ParentItem>();
LoadDummyParentItems();
}
private ICommand _filterChildrenCommand;
public ICommand FilterChildrenCommand => _filterChildrenCommand ?? (_filterChildrenCommand = new RelayCommand(param => FilterChildren((string)param), param => CanFilterChildren((string)param)));
private bool CanFilterChildren(string filter) => SelectedParentItem != null && filter.Length > 0;
private void FilterChildren(string filter)
{
CollectionViewSource.GetDefaultView(SelectedParentItem.ChildItems).Filter = item => (item as string).Contains(filter);
}
private void LoadDummyParentItems()
{
for (var i = 0; i < 20; i++)
{
ParentItems.Add(new ParentItem()
{
Name = $"Parent Item {i}",
CreatedOn = DateTime.Now.AddHours(i),
IsActive = i % 2 == 0 ? true : false,
Status = i % 2 == 0 ? ParentItemStatus.Status_Two : ParentItemStatus.Status_One,
ChildItems = new List<string>() { $"Child one_{i}", $"Child two_{i}", $"Child three_{i}", $"Child four_{i}" }
});
}
}
}
MainWindow.xaml.cs:
public partial class MainWindow : Window
{
private readonly MainWindowViewModel _viewModel;
public MainWindow()
{
InitializeComponent();
_viewModel = new MainWindowViewModel();
this.DataContext = _viewModel;
}
}
MainWindow.xaml:
<Window x:Class="FilteringDemo.Views.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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:FilteringDemo.Views"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=".25*"/>
<ColumnDefinition Width=".75*"/>
</Grid.ColumnDefinitions>
<ListView x:Name="ItemList" Grid.Column="0" Margin="2" ItemsSource="{Binding ParentItems}" SelectedItem="{Binding SelectedParentItem, Mode=OneWayToSource}" SelectionMode="Single">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="1*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding ElementName=ItemList, Path=SelectedItem.Name}" Margin="2"/>
<TextBlock Grid.Column="1" Text="{Binding ElementName=ItemList, Path=SelectedItem.CreatedOn}" Margin="2"/>
<TextBlock Grid.Column="2" Text="{Binding ElementName=ItemList, Path=SelectedItem.IsActive}" Margin="2"/>
<TextBlock Grid.Column="3" Text="{Binding ElementName=ItemList, Path=SelectedItem.Status}" Margin="2"/>
</Grid>
<ListView Grid.Row="1" Margin="2" ItemsSource="{Binding SelectedParentItem.ChildItems}" />
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox x:Name="ChildFilterInput" Grid.Column="0" Margin="2">
<TextBox.InputBindings>
<KeyBinding Command="{Binding FilterChildrenCommand}" CommandParameter="{Binding ElementName=ChildFilterInput, Path=Text}" Key="Return" />
</TextBox.InputBindings>
</TextBox>
<Button Grid.Column="1" Content="Filter" Width="100" Margin="2" Command="{Binding FilterChildrenCommand}" CommandParameter="{Binding ElementName=ChildFilterInput, Path=Text}"/>
</Grid>
</Grid>
</Grid>
</Window>
ViewModelBase.cs:
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected bool SetProperty<T>(ref T field, T newValue, [CallerMemberName] string propertyName = null)
{
if (!EqualityComparer<T>.Default.Equals(field, newValue))
{
field = newValue;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
return true;
}
return false;
}
}
RelayCommand.cs:
public class RelayCommand : ICommand
{
private Predicate<object> _canExecuteMethod;
private Action<object> _executeMethod;
public RelayCommand(Action<object> executeMethod, Predicate<object> canExecuteMethod = null)
{
_executeMethod = executeMethod;
_canExecuteMethod = canExecuteMethod;
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public bool CanExecute(object parameter)
{
return _canExecuteMethod == null ? true : _canExecuteMethod(parameter);
}
public void Execute(object parameter)
{
_executeMethod(parameter);
}
}
ParentItem.cs:
public class ParentItem
{
public string Name { get; set; }
public List<string> ChildItems { get; set; }
public DateTime CreatedOn { get; set; }
public bool IsActive { get; set; }
public ParentItemStatus Status { get; set; }
}
public enum ParentItemStatus
{
Status_One,
Status_Two
}
App.xaml:
<Application x:Class="FilteringDemo.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:FilteringDemo"
StartupUri="Views/MainWindow.xaml">
<Application.Resources>
</Application.Resources>
</Application>
Note: The MainWindow.xaml file was moved into a "Views" folder, so I'm including the App.xaml with the updated StartupUri in case anyone is trying to copy and paste.
I need to bind the property SelectedItem of a datagrid called dg_item in the class ItemList to a textbox in the page ItemViewMenu.
I have declared the DataContext in in the ItemViewMenu class as followed:
ItemViewMenu Menu = this;
ItemList List = PageResources.ItemListInstance;
this.DataContext = new { Menu, List };
This is the PageResources.ItemListInstance:
class PageResources
{
private static ItemList _itemListInstance;
public static ItemList ItemListInstance
{
get
{
if (_itemListInstance == null)
_itemListInstance = new ItemList();
return _itemListInstance;
}
}
}
The dg_item datagrid contains a list from the type Item which contains
the following property I need to access:
public class Item
{
private string _manufacturerName;
public string ManufacturerName
{
get
{
return _manufacturerName;
}
set
{
if (value != null)
{
_manufacturerName= value;
}
}
}
...
}
And I'm trying to access the Property in the "ItemViewMenu.xaml" like that:
<Grid DataContext="{Binding Source={StaticResource List.dg_item}, Path=SelectedItem}" Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0.3*" />
<ColumnDefinition Width="0.7*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock x:Name="txt_manufacturer" Grid.Row="0" Grid.Column="0" HorizontalAlignment="Left" Text="Manufacturer: " Margin="5,2" VerticalAlignment="Center" FontSize="13"/>
<TextBox x:Name="tb_manufacturer" Text="{Binding ManufacturerName}" Grid.Row="0" Grid.Column="1" Margin="5,2" FontSize="13" IsReadOnly="True" VerticalContentAlignment="Center" MaxHeight="50" />
...
</Grid>
But this doesn't work. So how do I access the property ManufacturerName?
In UWP Xaml,I have a ListView. In the ListView i use a DataTemplate with a stack panel,
I want change backgrund color of stackpanel when selectedItem is True
I do want to do it in Xaml
the other words,
Change DataTemplate for Listview item if selected
I do want to do it in Xaml
code in Xaml:
<ListView.ItemTemplate >
<DataTemplate x:Name="mydt">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="40" />
</Grid.ColumnDefinitions>
<StackPanel x:Name="MyStack">
<ContentControl Content="{Binding rtb}"
q:APGolAyah.MyProperty="{Binding AyahNo}" />
<TextBlock Text="{Binding Text}"
TextWrapping="Wrap" />
</StackPanel>
<Image Grid.Column="1"
Source="{Binding HezbNo,Converter={StaticResource HezbNoToIconConverter}}"
Width="25" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
As you've found that in UWP Style.Triggers cannot be used, and I understand that you want to do this work pure in xaml, but sorry to say that there is no such pure way now.
Since you only want to change the background color of the StackPanel when the item is selected, I think it is not needed to changed the whole DataTemplate, I'm writing the answer here to introduce a method which uses Converter for data binding and a little bit c# code here since maybe you interest in this method.
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid.Resources>
<local:BoolToBrushConverter x:Key="cvt" />
</Grid.Resources>
<ListView x:Name="listview" ItemsSource="{x:Bind Collection, Mode=OneWay}"
SelectionChanged="listview_SelectionChanged">
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="40" />
</Grid.ColumnDefinitions>
<StackPanel Background="{Binding IsSelected, Converter={StaticResource cvt}}">
<TextBlock Text="{Binding Name}" />
</StackPanel>
<TextBlock Grid.Column="1" Text="{Binding Age}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
Code behind and data model:
private ObservableCollection<Model> Collection = new ObservableCollection<Model>();
protected override void OnNavigatedTo(NavigationEventArgs e)
{
for (int i = 0; i < 50; i++)
{
Collection.Add(new Model { Name = "Name " + i + ", ", Age = i });
}
}
private void listview_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
foreach (Model item in e.AddedItems)
{
item.IsSelected = true;
}
foreach (Model item in e.RemovedItems)
{
item.IsSelected = false;
}
}
public class Model : INotifyPropertyChanged
{
public string Name { get; set; }
public int Age { get; set; }
private bool _IsSelected;
public bool IsSelected
{
get { return _IsSelected; }
set
{
if (value != _IsSelected)
{
_IsSelected = value;
OnPropertyChanged();
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName]string propertyName = "")
{
if (this.PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Converter:
public class BoolToBrushConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
bool? b = (bool?)value;
if (b == true)
return new SolidColorBrush(Colors.BlueViolet);
return new SolidColorBrush(Colors.Transparent);
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
And if you also interest in changing DataTemplate, you may refer to my answer in another case: UWP ListView: How to expand an item when select it?.
This is an example exhibiting the behaviour I'm having trouble with. I have a datagrid which is bound to an observable collection of records in a viewmodel. In the datagrid I have a DataGridTemplateColumn holding a combobox which is populated from a list in the viewmodel. The datagrid also contains text columns. There are some textboxes at the bottom of the window to show the record contents.
<Window x:Class="Customer.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Customer"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<local:SelectedRowConverter x:Key="selectedRowConverter"/>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="8*"/>
<RowDefinition Height="3*"/>
</Grid.RowDefinitions>
<DataGrid x:Name="dgCustomers" AutoGenerateColumns="False"
ItemsSource="{Binding customers}" SelectedItem="{Binding SelectedRow,
Converter={StaticResource selectedRowConverter}, Mode=TwoWay}"
CanUserAddRows="True" Grid.Row="0" SelectionChanged="dgCustomers_SelectionChanged">
<DataGrid.Columns>
<DataGridTemplateColumn Width="Auto" Header="Country">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox x:Name="cmbCountry" ItemsSource="{Binding DataContext.countries,
RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
DisplayMemberPath="name" SelectedValuePath="name" Margin="5"
SelectedItem="{Binding DataContext.SelectedCountry,
RelativeSource={RelativeSource AncestorType={x:Type Window}}, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}" SelectionChanged="cmbCountry_SelectionChanged" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Name" Binding="{Binding name}" Width="1*"/>
<DataGridTextColumn Header="Phone" Binding="{Binding phone}" Width="1*"/>
</DataGrid.Columns>
</DataGrid>
<Grid x:Name="grdDisplay" DataContext="{Binding ElementName=dgCustomers}" Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="2" Content="Country:" VerticalAlignment="Center" HorizontalAlignment="Right"/>
<Label Grid.Column="4" Content="Code:" VerticalAlignment="Center" HorizontalAlignment="Right"/>
<BulletDecorator Grid.Column="0">
<BulletDecorator.Bullet>
<Label Content="Name:" VerticalAlignment="Center" HorizontalAlignment="Right"/>
</BulletDecorator.Bullet>
<TextBox x:Name="txtId" Text="{Binding ElementName=dgCustomers, Path=SelectedItem.name}" Margin="5,5,5,5"/>
</BulletDecorator>
<BulletDecorator Grid.Column="1">
<BulletDecorator.Bullet>
<Label Content="Code:" VerticalAlignment="Center" HorizontalAlignment="Right"/>
</BulletDecorator.Bullet>
<TextBox x:Name="txtCode" Text="{Binding ElementName=dgCustomers, Path=SelectedItem.countryCode}" Margin="5,5,5,5"/>
</BulletDecorator>
<BulletDecorator Grid.Column="2">
<BulletDecorator.Bullet>
<Label Content="Phone:" VerticalAlignment="Center" HorizontalAlignment="Right"/>
</BulletDecorator.Bullet>
<TextBox x:Name="txtPhone" Text="{Binding ElementName=dgCustomers, Path=SelectedItem.phone}" Margin="5,5,5,5"/>
</BulletDecorator>
</Grid>
</Grid>
</Window>
Initially there are no records so the datagrid is empty and shows just one line containing the combobox. If the user enters data into the text columns first then a record is added to the collection and the combobox value can be added to the record. However, if the user selects the combobox value first, then the value disappears when another column is selected. How do I get the combobox data added to the record if it is selected first?
Codebehind:
public partial class MainWindow : Window
{
public GridModel gridModel { get; set; }
public MainWindow()
{
InitializeComponent();
gridModel = new GridModel();
//dgCustomers.DataContext = gridModel;
this.DataContext = gridModel;
}
private void cmbCountry_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ComboBox c = sender as ComboBox;
Debug.Print("ComboBox selection changed, index is " + c.SelectedIndex + ", selected item is " + c.SelectedItem);
}
}
The Record class:
public class Record : ViewModelBase
{
private string _name;
public string name
{
get { return _name; }
set
{
_name = value;
OnPropertyChanged("name");
}
}
private string _phone;
public string phone
{
get { return _phone; }
set
{
_phone = value;
OnPropertyChanged("phone");
}
}
private int _countryCode;
public int countryCode
{
get { return _countryCode; }
set
{
_countryCode = value;
OnPropertyChanged("countryCode");
}
}
}
Country class:
public class Country : ViewModelBase
{
private string _name;
public string name
{
get { return _name; }
set
{
_name = value;
OnPropertyChanged("name");
}
}
private int _id;
public int id
{
get { return _id; }
set
{
_id = value;
OnPropertyChanged("id");
}
}
private int _code;
public int code
{
get { return _code; }
set
{
_code = value;
OnPropertyChanged("code");
}
}
public override string ToString()
{
return _name;
}
}
GridModel:
public class GridModel : ViewModelBase
{
public ObservableCollection<Record> customers { get; set; }
public List<Country> countries { get; set; }
public GridModel()
{
customers = new ObservableCollection<Record>();
countries = new List<Country> { new Country { id = 1, name = "England", code = 44 }, new Country { id = 2, name = "Germany", code = 49 },
new Country { id = 3, name = "US", code = 1}, new Country { id = 4, name = "Canada", code = 11 }};
}
private Country _selectedCountry;
public Country SelectedCountry
{
get
{
return _selectedCountry;
}
set
{
_selectedCountry = value;
_selectedRow.countryCode = _selectedCountry.code;
OnPropertyChanged("SelectedRow");
}
}
private Record _selectedRow;
public Record SelectedRow
{
get
{
return _selectedRow;
}
set
{
_selectedRow = value;
Debug.Print("Datagrid selection changed");
OnPropertyChanged("SelectedRow");
}
}
}
Converters:
class Converters
{
}
public class SelectedRowConverter : 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 Record)
return value;
return new Customer.Record();
}
}
ViewModelBase:
public class ViewModelBase : INotifyPropertyChanged
{
public ViewModelBase()
{
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
}
The behaviour you are seeing is as expected. The reason behind it is that the ComboBox ItemsSource as well as SelectedItem both are bound to Properties of the Window's DataContext while the other columns are bound to your DataGrid's ItemsSource. Hence when you modify the columns other than the dropdown the data is added to the observable collection.
What you can do is after a value is selected from the drop down you need to add a record yourself (possibly by calling a function from your SelectedCountry property)
EDIT
Based on your code I made a working model making as little changes as possible to your existing code. I could not use the converter as I did not have the details of the class Customer
Xaml
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="8*"/>
<RowDefinition Height="3*"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0">
<Button HorizontalAlignment="Right" Content="Add User" Margin="0,2,2,2" Command="{Binding AddUserCommand}"/>
<DataGrid x:Name="dgCustomers"
AutoGenerateColumns="False"
ItemsSource="{Binding customers}"
SelectedItem="{Binding SelectedRow}"
SelectionUnit="FullRow"
CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTemplateColumn Width="Auto" Header="Country">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox Focusable="False"
ItemsSource="{Binding DataContext.countries, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}}"
DisplayMemberPath="name"
SelectedValuePath="code"
SelectedValue="{Binding countryCode, UpdateSourceTrigger=PropertyChanged}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Name" Binding="{Binding name, UpdateSourceTrigger=PropertyChanged}" Width="1*"/>
<DataGridTextColumn Header="Phone" Binding="{Binding phone, UpdateSourceTrigger=PropertyChanged}" Width="1*"/>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
<Grid x:Name="grdDisplay" DataContext="{Binding ElementName=dgCustomers}" Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="2" Content="Country:" VerticalAlignment="Center" HorizontalAlignment="Right"/>
<Label Grid.Column="4" Content="Code:" VerticalAlignment="Center" HorizontalAlignment="Right"/>
<BulletDecorator Grid.Column="0">
<BulletDecorator.Bullet>
<Label Content="Name:" VerticalAlignment="Center" HorizontalAlignment="Right"/>
</BulletDecorator.Bullet>
<TextBox x:Name="txtId" Text="{Binding ElementName=dgCustomers, Path=SelectedItem.name}" Margin="5,5,5,5"/>
</BulletDecorator>
<BulletDecorator Grid.Column="1">
<BulletDecorator.Bullet>
<Label Content="Code:" VerticalAlignment="Center" HorizontalAlignment="Right"/>
</BulletDecorator.Bullet>
<TextBox x:Name="txtCode" Text="{Binding ElementName=dgCustomers, Path=SelectedItem.countryCode}" Margin="5,5,5,5"/>
</BulletDecorator>
<BulletDecorator Grid.Column="2">
<BulletDecorator.Bullet>
<Label Content="Phone:" VerticalAlignment="Center" HorizontalAlignment="Right"/>
</BulletDecorator.Bullet>
<TextBox x:Name="txtPhone" Text="{Binding ElementName=dgCustomers, Path=SelectedItem.phone}" Margin="5,5,5,5"/>
</BulletDecorator>
</Grid>
</Grid>
Your GridModel class
public class GridModel : ViewModelBase
{
public ObservableCollection<Record> customers { get; set; }
public ObservableCollection<Country> countries
{
get;
private set;
}
public GridModel()
{
customers = new ObservableCollection<Record> { };
AddUserCommand = new RelayCommand(AddNewUser);
countries = new ObservableCollection<Country>
{
new Country { id = 1, name = "England", code = 44 },
new Country { id = 2, name = "Germany", code = 49 },
new Country { id = 3, name = "US", code = 1},
new Country { id = 4, name = "Canada", code = 11 }
};
}
private void AddNewUser()
{
customers.Add(new Record());
}
public ICommand AddUserCommand { get; set; }
private Record _selectedRow;
public Record SelectedRow
{
get
{
return _selectedRow;
}
set
{
_selectedRow = value;
Debug.Print("Datagrid selection changed");
OnPropertyChanged("SelectedRow");
}
}
}
I have used MVVMLight toolkit which contains RelayCommand. You can also define your own ICommand implementation and use it instead of the toolkit
EDIT 2
Fixed the bug introduced by me which would prevent the combobox from displaying the Country if the data comes from the data base. The improved code does not require any converter either