I have a WPF application, that establishes a connection to a different computer. Inside my application, I have a combo box, where the user can enter a hostname of a computer and then connect to this computer. Now once the connection was established the hostname the user entered gets saved into an Observable Collection which is bound to the combo box, so the next time he wants to connect to the same host, he can choose it directly from the combo box.
I have implemented a favorite list. which is a separate observable collection that I too want to bind to the same combo box, so the user can either choose either a favorite or a history item.
In the dropdown list of the combo box I would like 2 Groupings with a Header, something like this:
[Favorites]
My Favourite Host | myfavhost.com
My 2nd Fav | my2ndfav.com
Secretly My Fav | secretlymyfav.com
[History]
hostioncevisited.com
whyamihere.com
thanksforhelping.com
Now I don't really know how to go about that. Is there a way to bind multiple items sources to the combobox, or would I have to merge the two observable collections before I bind them to the combo box?
These are my observable collections
public ObservableCollection<string> HistoryItems { get; set; } = new ObservableCollection<string>();
public static ObservableCollection<FavoriteItem> FavoriteItems { get; set; } = new ObservableCollection<FavoriteItem>();
Here is my FavoriteItem Class
public class FavoriteItem : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string hostName;
private string description;
public FavoriteItem(){}
public FavoriteItem(string _hostName, string _description)
{
hostName = _hostName;
description = _description;
}
public string Hostname
{
get { return hostName; }
set
{
hostName = value;
OnPropertyChanged("Hostname");
}
}
public string Description
{
get { return description; }
set
{
description = value;
OnPropertyChanged("Description");
}
}
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
public override string ToString()
{
return string.Format("{0} | {1}", description, hostName);
}
}
Here is the XAML for the combo box
XAML
<ComboBox Name="cbHostName" Style="{StaticResource ComboBoxLarge}" Text="{Binding HostName}" ItemsSource="{Binding HistoryItems}"
MinWidth="300" MaxWidth="300" IsEditable="True" Margin="0,0,15,0" VerticalAlignment="Center" materialDesign:HintAssist.Hint="Computer, IP or HostProfileName"/>
You can use CompositeCollection to bind multiple collections to the same source.
Here is an example.
The disadvantage is that I don't think grouping is possible in this scenario (at least not easily).
The alternative would be to have only one list, of objects implementing the same interface, with some property to distinguish type of item, e.g.:
public interface IHost : INotifyPropertyChanged
{
string HostType { get; }
string Hostname { get; set; }
string DisplayText { get; set; }
}
public class HistoryItem : IHost
{
public event PropertyChangedEventHandler PropertyChanged;
public string HostType => "History";
public string Hostname { get; set; }
public string DisplayText => Hostname;
}
public class FavoriteItem : IHost
{
public event PropertyChangedEventHandler PropertyChanged;
public string HostType => "Favorites";
public string Hostname { get; set; }
public string Description { get; set; }
public string DisplayText => Description == null ? Hostname : $"{Description} | {Hostname}";
//other properties....
}
As I find working directly with ObservableCollection annoying, I tend to use a wrapper for it (code at the bottom). It deals with some common issues, such as possible memory leaks and raising CollectionChanged events unnecessary while adding multiple items. It also provides easy access to grouping, sorting, filtering, current item and CurrentChanged & CurrentChanging events from codebehind.
In ViewModel:
public ViewableCollection<IHost> MyItems { get; set; }
Initializing the collection:
this.MyItems = new ViewableCollection<IHost>();
// decide how your items will be sorted (important: first sort groups, then items in groups)
this.MyItems.View.SortDescriptions.Add(new SortDescription("HostType", ListSortDirection.Ascending)); // sorting of groups
this.MyItems.View.SortDescriptions.Add(new SortDescription("Hostname", ListSortDirection.Ascending)); // sorting of items
PropertyGroupDescription groupDescription = new PropertyGroupDescription("HostType");
this.MyItems.View.GroupDescriptions.Add(groupDescription);
this.MyItems.View.CurrentChanged += MyItems_CurrentChanged;
this.MyItems.AddRange(new IHost[] {
new HistoryItem { Hostname = "ccc" },
new HistoryItem { Hostname = "aaa" },
new HistoryItem { Hostname = "xxx" },
new FavoriteItem { Hostname = "vvv" },
new FavoriteItem { Hostname = "bbb" },
new FavoriteItem { Hostname = "ttt" } });
This code will execute when the item is selected:
private void MyItems_CurrentChanged(object sender, EventArgs e)
{
Console.WriteLine("Selected item: " + this.MyItems.CurrentItem?.Hostname);
}
Here is the xaml of ComboBox with grouping (using ViewableCollection, you need to bind ItemsSource to MyItems.View instead of directly to MyItems):
<ComboBox ItemsSource="{Binding MyItems.View, Mode=OneWay}"
IsSynchronizedWithCurrentItem="True"
DisplayMemberPath="DisplayText">
<ComboBox.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Items.CurrentItem.HostType, StringFormat=[{0}]}"/>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ComboBox.GroupStyle>
</ComboBox>
result:
[DoNotNotify]
public class ViewableCollection<T> : ObservableCollection<T>
{
private ListCollectionView _View;
public ViewableCollection(IEnumerable<T> items)
: base(items) { }
public ViewableCollection()
: base() { }
[XmlIgnore]
public ListCollectionView View
{
get
{
if (_View == null)
{
_View = new ListCollectionView(this);
_View.CurrentChanged += new EventHandler(InnerView_CurrentChanged);
}
return _View;
}
}
[XmlIgnore]
public T CurrentItem
{
get
{
return (T)this.View.CurrentItem;
}
set
{
this.View.MoveCurrentTo(value);
}
}
private void InnerView_CurrentChanged(object sender, EventArgs e)
{
this.OnPropertyChanged(new PropertyChangedEventArgs("CurrentItem"));
}
public void AddRange(IEnumerable<T> range)
{
if (range == null)
throw new ArgumentNullException("range");
foreach (T item in range)
{
this.Items.Add(item);
}
this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
public void ReplaceItems(IEnumerable<T> range)
{
if (range == null)
throw new ArgumentNullException("range");
this.Items.Clear();
foreach (T item in range)
{
this.Items.Add(item);
}
this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
public void RemoveItems(IEnumerable<T> range)
{
if (range == null)
throw new ArgumentNullException("range");
foreach (T item in range)
{
this.Items.Remove(item);
}
this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
public void ClearAll()
{
IList old = this.Items.ToList();
base.Items.Clear();
this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
public void CallCollectionChaged()
{
this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
// necessary for xml easy serialization using [XmlArray] attribute
public static implicit operator List<T>(ViewableCollection<T> o)
{
return o == null ? default(List<T>) : o.ToList();
}
// necessary for xml easy serialization using [XmlArray] attribute
public static implicit operator ViewableCollection<T>(List<T> o)
{
return o == default(List<T>) || o == null ? new ViewableCollection<T>() : new ViewableCollection<T>(o);
}
}
The above code is a working example. I'm using nuget package PropertyChanged2.Fody to inject PropertyChanged notifications.
No you cannot bind multiple collections to ItemsSource, you have to merge them
Related
I have a problem with my custom stacklayout which populates the stacklayout correctly but does not recognizes any changes of any item in the bound observable collection..
This is the code I use for the bindable stacklayout:
public class BindableStackLayout : StackLayout
{
private readonly Label _header;
public BindableStackLayout()
{
_header = new Label();
Children.Add(_header);
}
public IEnumerable ItemsSource
{
get => (IEnumerable)GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}
public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create(nameof(ItemsSource), typeof(IEnumerable),
typeof(BindableStackLayout), propertyChanged: (bindable, oldValue, newValue) => ((BindableStackLayout)bindable).PopulateItems());
public DataTemplate ItemDataTemplate
{
get => (DataTemplate)GetValue(ItemDataTemplateProperty);
set => SetValue(ItemDataTemplateProperty, value);
}
public static readonly BindableProperty ItemDataTemplateProperty = BindableProperty.Create(nameof(ItemDataTemplate),
typeof(DataTemplate), typeof(BindableStackLayout));
public string Title
{
get => (string)GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
public static readonly BindableProperty TitleProperty = BindableProperty.Create(nameof(Title), typeof(string),
typeof(BindableStackLayout), propertyChanged: (bindable, oldValue, newValue) => ((BindableStackLayout)bindable).PopulateHeader());
private void PopulateItems()
{
if (ItemsSource == null)
return;
foreach (var item in ItemsSource)
{
var itemTemplate = ItemDataTemplate.CreateContent() as Xamarin.Forms.View;
itemTemplate.BindingContext = item;
Children.Add(itemTemplate);
}
}
private void PopulateHeader() => _header.Text = Title;
}
Which is used like you can find here:
<ContentView.Content>
<h:BindableStackLayout ItemsSource="{Binding MenuHotKeys, Mode=TwoWay}"
Style="{StaticResource MenuControlStackLayout}">
<h:BindableStackLayout.ItemDataTemplate>
<DataTemplate>
<Button Text="{Binding DataA}"
Command="{Binding Path=BindingContext.MenuControlCommand, Source={x:Reference InternalMenuControl}}"
CommandParameter="{Binding .}"
Style="{StaticResource MenuControlButton}"/>
</DataTemplate>
</h:BindableStackLayout.ItemDataTemplate>
</h:BindableStackLayout>
</ContentView.Content>
And in the viewmodel I have this code:
private ObservableCollection<ConfigMenuItem> _menuHotKeys;
public ObservableCollection<ConfigMenuItem> MenuHotKeys
{
get => _menuHotKeys;
set => SetValue(ref _menuHotKeys, value);
}
And the change is here:
private async void MenuControlButtonPressed(object sender)
{
var menuItem = sender as ConfigMenuItem;
if (menuItem.ItemId == _expanderId)
{
// toggle expanded menu visibility
var expander = _menuHotKeys.FirstOrDefault(p => p.ItemId == _expanderId);
var buffer = expander.DataA;
expander.DataA = expander.DataB;
expander.DataB = buffer;
}
else
{
await NavigationHandler.NavigateToMenuItem(menuItem);
}
}
As you can see, I want to toggle the name of the bound button, but the changes does not appear.
I think I have to change something in bindable stacklayout class, but what?
Maybe you can help
#INPC answers:
The ConfigMenuItem in the Collection derives from:
public abstract class BaseObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void SetValue<T>(ref T field, T value, Expression<Func<T>> property)
{
if (!ReferenceEquals(field, value))
{
field = value;
OnPropertyChanged(property);
}
}
protected virtual void OnPropertyChanged<T>(Expression<Func<T>> changedProperty)
{
if (PropertyChanged != null)
{
string name = ((MemberExpression)changedProperty.Body).Member.Name;
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
}
and the viewmodel derives from:
public abstract class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual void SetValue<T>(ref T privateField, T value, [CallerMemberName] string propertyName = null)
{
if (!EqualityComparer<T>.Default.Equals(privateField, value))
{
privateField = value;
OnPropertyChanged(propertyName);
}
return;
}
}
As requested in comments the ConfigMenuItem class, code of BaseObject see in the code upside:
public class ConfigMenuItem : BaseObject, IConfigMenuItem
{
public int ItemId
{
get;
set;
}
public int Position
{
get;
set;
}
public string Name
{
get;
set;
}
public string DataA
{
get;
set;
}
public string DataB
{
get;
set;
}
public bool IsEnabled
{
get;
set;
}
public bool IsHotKey
{
get;
set;
}
public bool IsCustomMenuItem
{
get;
set;
}
public override string ToString()
{
return $"{Name} ({DataA} | {DataB ?? "null"})";
}
}
The problem is caused by the fact that although your ConfigMenuItem class derives from BaseObject, all its properties are plain properties and do not tirgger PropertyChanged event. You have to rewrite the properties to have a backing field and to trigger the event in their setter. For example:
private string _dataA;
public string DataA
{
get => _dataA;
set => SetValue(ref _dataA, value);
}
My example is using the SetValue method from BaseViewModel, and I actually think the BaseObject class is redundant and you could just use BaseViewModel instead. Using [CallerMemberName] for property is much more convenient than having additional logic for Expression<Func<T>>.
I have a DataGrid in my current WPF Application which I would like to bind to a ViewModel that holds a ObservableCollection. The user can enter search values in some TextBoxes and after enter has been hit I am performing an query to our database that retunrs a table of records. From these records I am populate the data for the ObservableCollection. I am now struggeling now that the datagrid is not displaying the data.
I have read a howl bunch of posts about the binding but I am still missing something I think.
Product.cs
public class Product : InotifyPropertyChanged, IEditableObject
{
public string Title { get; set; } = "";
//public Product()
//{
//}
private ProductViewModel _productViewModel = new ProductViewModel();
public ProductViewModel productViewModel { get { return _productViewModel; } set { _productViewModel = value; } }
public DataTable ProductsTable { get; set; }
public void GetProducts(string filter)
{
//< --doing some stuff to fill the table-->
foreach (DataRow row in ProductsTable.Rows)
{
productViewModel.Products.Add(new Product
{
Title = (string)row["TITLE"],
});
}
}
}
ProductViewModel.cs
public class ProductViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private Product _SelectedProduct;
private ObservableCollection<Product> _Products = new ObservableCollection<Product>();
public ObservableCollection<Product> Products { get { return _Products; } set { _Products = value; } }
public ProductViewModel()
{
}
public void NotifyPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
ProductWindow.xaml
<DataGrid
Name="ProductsGrid"
AutoGenerateColumns="False"
ItemsSource="{Binding Products, Mode=TwoWay, NotifyOnSourceUpdated=True}"
SelectedItem="{Binding SelectedProduct, Mode=TwoWay}"
CanUserAddRows="False" SelectionUnit="FullRow"
VerticalAlignment="Stretch"
Grid.Row="0"
Margin="10,10,10,10"
>
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Title}" Header="Title"></DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
ProductWindow.xaml.cs
public partial class ProductWindow : Page
{
public object DialogResult { get; private set; }
//public ProductViewModel ProductViewModel;
public ProductWindow()
{
InitializeComponent();
DataContext = new ProductViewModel();//stackflow
//var ProductViewModel = products.ProductViewModel;
//ProductsGrid.DataContext = new ProductViewModel();
}
public ProductViewModel ViewModel => DataContext as ProductViewModel;
private void OnKeydownHandler(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
var tb = sender as TextBox;
Product products = new Product();
string filter = "";//performing some ifelse to create filter
products.GetProducts(filter);
//ProductsGrid.DataContext = products.ProductsTable;
//ProductsGrid.DataContext = products.productViewModel;
}
else if (e.Key == Key.Escape)
{
ProductsGrid.DataContext = null;
foreach (TextBox tb in FindVisualChildren<TextBox>(this))
{
// do something with tb here
tb.Text = "";
}
}
}
}
If DataContext is a ProductViewModel, and the Products collection of that ProductViewModel is populated, you will see rows in your DataGrid. I've tested that. It appears that the viewmodel you're giving it may not have any rows.
That said, there's a problem with your design:
Product creates a ProductViewModel. ProductViewModel creates a collection of Product. Each Product, as I just said, creates a ProductViewModel. Which creates a collection of Product. They keep creating each other until you get a StackOverflowException. If you're not seeing that, you must be calling GetProducts() from somewhere else.
But there's no need for Product to own a copy of ProductViewModel. That's like adding a car to each wheel on your car.
So let's do this instead: ProductViewModel owns a collection of Product. Just that. And we'll call GetProducts() to make sure we get some items in the grid. Your binding is fine. You just weren't populating the collection.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ProductViewModel();
}
// Now you can call ViewModel.GetProducts(filterString) from an event handler.
// It would be more "correct" to use a Command, but let's take one step at a time.
public ProductViewModel ViewModel => DataContext as ProductViewModel;
}
Viewmodels
// You didn't include any implementation of IEditableObject. I presume
// you can add that back in to this version of the class.
public class Product : INotifyPropertyChanged, IEditableObject
{
// You weren't raising PropertyChanged here, or anywhere at all.
// In every setter on a viewmodel, you need to do that.
private string _title = "";
public string Title {
get => _title;
set
{
if (_title != value)
{
_title = value;
NotifyPropertyChanged(nameof(Title));
}
}
}
public Product()
{
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class ProductViewModel : INotifyPropertyChanged
{
public ProductViewModel()
{
GetProducts("");
}
public event PropertyChangedEventHandler PropertyChanged;
private Product _SelectedProduct;
public Product SelectedProduct
{
get { return _SelectedProduct; }
set
{
if (value != _SelectedProduct)
{
_SelectedProduct = value;
NotifyPropertyChanged(nameof(SelectedProduct));
}
}
}
public DataTable ProductsTable { get; set; }
public void GetProducts(string filter)
{
//< --doing some stuff to fill the table-->
Products.Clear();
foreach (DataRow row in ProductsTable.Rows)
{
Products.Add(new Product
{
Title = (string)row["TITLE"],
});
}
}
private ObservableCollection<Product> _Products = new ObservableCollection<Product>();
// This setter MUST raise PropertyChanged. See the Title property above for example.
public ObservableCollection<Product> Products { get { return _Products; } private set { _Products = value; } }
public void NotifyPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Update
Here's the problem: You create a new Product, which creates its own ProductsViewModel. Nothing is bound to any property of that viewmodel. You fill its collection and the DataGrid doesn't know or care, because you bound its ItemsSource to a property of a different object.
So use my suggestions above, particularly the ViewModel property of the window. I just made a change in ProductsViewModel.GetProducts() that you need to copy: Now it calls Products.Clear() before populating the collection.
if (e.Key == Key.Enter)
{
var tb = sender as TextBox;
// Don't create this
//Product products = new Product();
string filter = "";//performing some ifelse to create filter
ViewModel.GetProducts(filter);
}
else if (e.Key == Key.Escape)
{
// Setting the DataContext to null breaks everything. Never do that.
//ProductsGrid.DataContext = null;
// Instead, just clear the collection. It's an ObservableCollection so it will
// notify the DataGrid that it was cleared.
ViewModel.Products.Clear();
foreach (TextBox tb in FindVisualChildren<TextBox>(this))
{
// do something with tb here
tb.Text = "";
}
}
I have one gridcontrol with various fields which i havent mentioned in my code
<dxg:GridControl HorizontalAlignment="Stretch" Height="300" VerticalAlignment="Top" x:Name="grid1" AutoPopulateColumns="False" ItemsSource="{Binding Collection1}" >
<dxg:GridControl.View >
<dxg:TableView x:Name="TableView1" />
</dxg:GridControl.View>
.
.
.
.
I have another grid control on the same page with various fields
<dxg:GridControl HorizontalAlignment="Stretch" Height="250" VerticalAlignment="Top" x:Name="grid2" AutoPopulateColumns="False"
ItemsSource="{Binding ElementName="TableView1" ,path=Collection2.FocusedRow}" >
<dxg:GridControl.View >
<dxg:TableView x:Name="TableView2" />
</dxg:GridControl.View>
.
.
.
.
now collection1 Id is primary key and collection2 colID is foreign key both are having relationship with each other
Scenario here is if i select a row in grid1 all the corresponding records must be displayed in grid 2
public class myCollection: BindingList<orders>
{
public DataContext dc;
public myCollection(IList<orders> list)
: base(list)
{
}
protected override void RemoveItem(int index)
{
orders deleteItem = this.Items[index];
if (Dc.Order != null)
{
Dc.Order.DeleteOnSubmit(deleteItem);
}
base.RemoveItem(index);
}
}
My generic class for orders and generic class for master is the same
If I speak in terms of XAML properties, here you want to update ItemsSource property of 2nd Datagrid on basis of SelectedItem property of 1st Datagrid.
To achieve this, add a new property "SelectedItemDg1" in ViewModel which will hold the selection of 1st DataGrid. In Setter of this "SelectedItemDg1" property, set Collection2 as per your need.
Make sure to implement INotifyPropertyChanged interface and use ObservableCollection type for both the collections.
Following is the code sample for same :
Model Classes:
public class Country
{
public string CountryName { get; set; }
public int CountryId { get; set; }
public List<State> States { get; set; }
}
public class State
{
public string StateName { get; set; }
public int StateId { get; set; }
}
ViewModel :
public class MainWindowViewModel : INotifyPropertyChanged
{
public MainWindowViewModel()
{
CountriesCollection = new ObservableCollection<Country>();
StateCollection = new ObservableCollection<State>();
LoadData();
}
private ObservableCollection<Country> _CountriesCollection;
public ObservableCollection<Country> CountriesCollection
{
get { return _CountriesCollection; }
set
{
_CountriesCollection = value;
NotifyPropertyChanged("CountriesCollection");
}
}
private ObservableCollection<State> _StatesCollection;
public ObservableCollection<State> StateCollection
{
get { return _StatesCollection; }
set
{
_StatesCollection = value;
NotifyPropertyChanged("StateCollection");
}
}
private Country _SelectedCountry;
public Country SelectedCountry
{
get { return _SelectedCountry; }
set
{
_SelectedCountry = value;
if (_SelectedCountry != null && _SelectedCountry.States != null)
{
StateCollection = new ObservableCollection<State>(_SelectedCountry.States);
}
NotifyPropertyChanged("SelectedCountry");
}
}
private void LoadData()
{
if (CountriesCollection != null)
{
CountriesCollection.Add(new Country
{
CountryId = 1,
CountryName = "India",
States = new List<State>
{
new State { StateId = 1, StateName = "Gujarat"},
new State { StateId = 2, StateName = "Punjab"},
new State { StateId = 3, StateName = "Maharastra"}
}
});
CountriesCollection.Add(new Country
{
CountryId = 2,
CountryName = "Chine",
States = new List<State>
{
new State { StateId = 4, StateName = "Chine_State1"},
new State { StateId = 5, StateName = "Chine_State2"},
new State { StateId = 6, StateName = "Chine_State3"}
}
});
CountriesCollection.Add(new Country
{
CountryId = 3,
CountryName = "japan",
States = new List<State>
{
new State { StateId = 7, StateName = "Japan_State1"},
new State { StateId = 8, StateName = "Japan_State2"},
new State { StateId = 9, StateName = "Japan_State3"}
}
});
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(info));
}
}
XALM :
<StackPanel Orientation="Horizontal" >
<DataGrid AutoGenerateColumns="True"
Height="300" Width="300"
HorizontalAlignment="Left" Margin="30"
ItemsSource="{Binding CountriesCollection}"
SelectedItem="{Binding SelectedCountry}">
</DataGrid>
<DataGrid AutoGenerateColumns="True"
Height="300" Width="300"
HorizontalAlignment="Left" Margin="30"
ItemsSource="{Binding SelectedCountry.States}">
</DataGrid>
</StackPanel>
Here I have AutoGenerateColumns property of DataGrid but you have to change it as per your requirement.
I hope this sample code will make things easy to understand for you.
The simplest and cleanest way I found to do this sort of master-details binding on collections is to wrap ObservableCollection in a class, expose its ListCollectionView and bind your ItemsSource to it, like below (it has some extra code that is used to simplify xml serialization):
public class ViewableCollection<T> : ObservableCollection<T>
{
private ListCollectionView _View;
public ViewableCollection(IEnumerable<T> items)
: base(items) { }
public ViewableCollection()
: base() { }
[XmlIgnore]
public ListCollectionView View
{
get
{
if (_View == null)
{
_View = new ListCollectionView(this);
_View.CurrentChanged += new EventHandler(InnerView_CurrentChanged);
}
return _View;
}
}
[XmlIgnore]
public T CurrentItem
{
get
{
return (T)this.View.CurrentItem;
}
set
{
this.View.MoveCurrentTo(value);
}
}
private void InnerView_CurrentChanged(object sender, EventArgs e)
{
this.OnPropertyChanged(new PropertyChangedEventArgs("CurrentItem"));
}
public void AddRange(IEnumerable<T> range)
{
if (range == null)
throw new ArgumentNullException("range");
foreach (T item in range)
{
this.Items.Add(item);
}
this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, (IList)range.ToList()));
}
public void ReplaceItems(IEnumerable<T> range)
{
if (range == null)
throw new ArgumentNullException("range");
this.Items.Clear();
foreach (T item in range)
{
this.Items.Add(item);
}
this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
public void RemoveItems(IEnumerable<T> range)
{
if (range == null)
throw new ArgumentNullException("range");
foreach (T item in range)
{
this.Items.Remove(item);
}
this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, (IList)range.ToList()));
}
public void ClearAll()
{
IList old = this.Items.ToList();
base.Items.Clear();
this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, old));
}
public void CallCollectionChaged()
{
this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
// necessary for xml easy serialization using [XmlArray] attribute
public static implicit operator List<T>(ViewableCollection<T> o)
{
return o == null ? default(List<T>) : o.ToList();
}
// necessary for xml easy serialization using [XmlArray] attribute
public static implicit operator ViewableCollection<T>(List<T> o)
{
return o == default(List<T>) || o == null ? new ViewableCollection<T>() : new ViewableCollection<T>(o);
}
}
Then in your ViewModel (Remember to implement INotifyPropertyChanged, I use a nuget package Fody.PropertyChanged to automatically implement it on properties):
[ImplementPropertyChanged]
public class MyViewModel
{
public ViewableCollection<MySecondViewModel> Collection1 { get; set; }
public MyViewModel()
{
this.Collection1 = new ViewableCollection<MySecondViewModel>();
}
}
[ImplementPropertyChanged]
public class MySecondViewModel
{
public string MyColumn1 { get; set; }
public string MyColumn2 { get; set; }
public ViewableCollection<MyThirdViewModel> Collection2 { get; set; }
public MySecondViewModel()
{
this.Collection1 = new ViewableCollection<MyThirdViewModel>();
}
}
[ImplementPropertyChanged]
public class MyThirdViewModel
{
public string MyColumn1 { get; set; }
public string MyColumn2 { get; set; }
}
//...
this.DataContext = new MyViewModel();
Then, keeping your grids synchronized is as simple as this:
<DataGrid ItemsSource="{Binding Collection1.View, Mode=OneWay}"
IsSynchronizedWithCurrentItem="True" />
<DataGrid ItemsSource="{Binding Collection1.CurrentItem.Collection2.View, Mode=OneWay}"
IsSynchronizedWithCurrentItem="True" />
For example binding to a Column1 property in currently selected item in currently selected Collection2 will be:
<TextBlock Text="{Binding Collection1.CurrentItem.Collection2.CurrentItem.Column1}" />
Also, you can manage selection in your code behind, for example:
Collection1.CurrentItem=null;
will clear the selection on the collection.
You can also sort (and filter and group) the ViewableCollection from code behind like this:
Collection1.View.SortDescriptions.Add(new SortDescription("Column1",ListSortDirection.Ascending));
Just remember that you should't replace the whole ViewableCollection after it's been instantiated, just add/remove items from it (for this purpose there is the method AddRange and ReplaceItems for adding/replacing items in bulk without rising CollectionChanged events unnecessarily)
Hello I can't seem to figure out why my combox stays empty :/
On page load the first combox gets populated with countries (from JSON), the second combobox should populate when a country is selected in the first combobox. I'm trying to fetch the SelectedItem (country) as string in a property ... SelectedItem is of type ComboBoxItem ? I think that is where it goes wrong.
The (view)model where the sorting bindable properties are:
public class LocalityModel : NotifyProp
{
#region properties
private static List<LocalityJSON> dataList;
public List<LocalityJSON> DataList
{
get
{
return dataList;
}
set {
dataList = value;
RaisePropertyChanged("Landen");
RaisePropertyChanged("Gewesten");
}
}
public List<string> Landen
{
get { if (DataList == null) return null; return (from s in DataList orderby s.Land select s.Land).Distinct().ToList<string>(); }
}
public string SelectedLand { get; set; }
public List<string> Gewesten {
get { if (DataList == null) return null; return (from s in DataList where s.Land.Equals(SelectedLand) select s.Gewest).Distinct().ToList<string>(); }
}
#endregion
#region ctor
public LocalityModel()
{
FillDataList();
}
#endregion
#region methodes
public async void FillDataList()
{
if (DataList == null)
{
DataList = await EVNT.Entries();
}
}
#endregion
}
MainPage XAML (the bindings):
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" DataContext="{Binding Source={StaticResource LocalityModel}}">
...
<TextBlock x:Name="txbCountry" Style="{StaticResource InfoLabelCountry}" />
<ComboBox x:Name="cboCountry" Style="{StaticResource CountryBox}" ItemsSource="{Binding Landen}" SelectedItem="{Binding SelectedLand, Mode=TwoWay}" />
<TextBlock x:Name="txbGewest" Style="{StaticResource InfoLabelGewest}" />
<ComboBox x:Name="cboGewest" Style="{StaticResource GewestBox}" ItemsSource="{Binding Gewesten}" />
INotifyPropertyChanged:
public class NotifyProp : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
The model for the JSON:
public class LocalityJSON
{
public string FB_ID { get; set; }
public string Land { get; set; }
public string Gewest { get; set; }
public string City { get; set; }
}
The JSON deserialisation (less important for the question):
public class EVNT
{
public async static Task<List<LocalityJSON>> Entries()
{
using (var client = new HttpClient())
{
client.BaseAddress = new Uri(#"http://e-vnt.com/admin/core/api/");
HttpResponseMessage response = await client.GetAsync("localityApi");
if (response.IsSuccessStatusCode)
{
String s = await response.Content.ReadAsStringAsync();
List<LocalityJSON> entries = JsonConvert.DeserializeObject<List<LocalityJSON>>(s);
return entries;
}
else
return null;
}
}
}
In your SelectedLand Property setter you need to fire PropertyChanged event the for both SelectedLand and for Gewesten.
It would probably look something like this
private string _SelectedLand;
public string SelectedLand
{
get
{
return _SelectedLand;
}
set
{
_SelectedLand = value;
RaisePropertyChanged("SelectedLand");
RaisePropertyChanged("Gewesten");
}
}
if you don't fire PropertyChanged event for Gewesten then that combobox will not know to reload is values.
I am developing an app(MVVM pattern) for windows store using WCF service to receive data from database.
I want to data bind a list of categories into combobox, but it's not working for me, I searched the web and still didn't find a solution.
Class Category:
public Category(Category c)
{
this.Id=c.Id;
this.Name = c.Name;
}
public int Id { get; set; }
public string Name { get; set; }
Xaml:
<ComboBox x:Name="ChooseCategory"
ItemsSource="{Binding ListCategories}"
DisplayMemberPath="Name"
SelectedValuePath="Id"
SelectedValue="{Binding SelectedItem, Mode=TwoWay}"/>
ViewModel:
public ObservableCollection<Category> ListCategories { get; private set; }
in the OnNavigatedTo function:
var listCategory = await proxy.GetAllCategoriesAsync();
List<Category> list = new List<Category>();
foreach (var item in listCategory)
{
list.Add(new Category(item));
}
ListCategories = new ObservableCollection<Category>(list);
Anyone???
You need to implement INotifyPropertyChanged in order to let UI know that you have changed the ListCategories collection.
In your ViewModel, implement interface INotifyPropertyChanged
public class YourViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
private ObservableCollection<Category> _categories;
public ObservableCollection<Category> ListCategories
{
get { return _categories; }
set
{
if (_categories != value)
{
_categories = value;
OnPropertyChanged("ListCategories");
}
}
}