Event handler not firing on property changed - wpf

I have a WPF application and I want the Start button control only enabled if they have to have specified a value in the text box for 'Download Path'.
My ViewModel contains a property for my model "ConfigurationSettings" and an ICommand implementation (CommandImp) for the button:
public class MainWindowViewModel : BaseNotifyPropertyChanged
{
private ConfigurationSettings _configurationSettings { get; set; }
public ConfigurationSettings ConfigurationSettings
{
get
{
return _configurationSettings;
}
set
{
if (_configurationSettings != value)
{
_configurationSettings = value;
RaisePropertyChanged("ConfigurationSettings");
}
}
}
public CommandImp StartCommand { get; set; } // this is an implementation of ICommand
public MainWindowViewModel()
{
StartCommand = new CommandImp(OnStart, CanStart);
_configurationSettings = new ConfigurationSettings();
_configurationSettings.PropertyChanged += delegate (object o,
PropertyChangedEventArgs args)
{
StartCommand.RaiseCanExecuteChanged(); // break point here is never reached
};
}
private bool CanStart()
{
if (!String.IsNullOrEmpty(ConfigurationSettings.DownloadPath))
{
return true;
}
return false;
}
}
In my XAML I have a Start button and the with Command = "{Binding StartCommand}".
My ConfigurationSettings class just has a string for the DownloadPath which is bound to a textbox in the XAML:
public class ConfigurationSettings : BaseNotifyPropertyChanged
{
private string _downloadPath { get; set; }
public string DownloadPath
{
get { return _downloadPath; }
set
{
if (_downloadPath != value)
{
_downloadPath = value;
RaisePropertyChanged("DownloadPath"); // break point here IS reached
}
}
}
}
When the user enters a DownloadPath, I expect it to be triggering the PropertyChanged Event, and running my delegate method defined in the ViewModel constructor.
If I move the Command Button inside the ConfigurationSettings class I can do away with event subscription and just use StartCommand.RaiseCanExecuteChanged() right beneath RaisePropertyChanged("DownloadPath");. But I don't want the ICommand as part of my Model.
How can I trigger CanStart() when one of the properties of ConfigurationSettings changes?
UPDATE:
Here is the XAML for the text box binding:
<TextBlock Text="{Binding ConfigurationSettings.DownloadPath, Mode=TwoWay, UpdateSourceTrigger=LostFocus}" TextWrapping="WrapWithOverflow" />
And the button:
<Button Content="Start" Command="{Binding StartCommand}"></Button>
I should note that the bindings are working correctly. When I update the textblock, I can see in the ViewModel that ConfigurationSettings.DownloadPath is correctly being updated.
BaseNotifyPropertyChanged is an implementation of INotifyPropertyChanged like so:
public class BaseNotifyPropertyChanged : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string property)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
}
I don't seem to be having any issues with the property changed event. I can put a break point in here and it is hit when I update the DownloadPath text box. It's when I subscribe to this PropertyChanged event in my ViewModel constructor, my delegate method isn't firing.

Hate to answer my own question but the people commenting made me think about restructuring my question - which led me to the answer before needing to make another update.
The solution was to move my event subscription inside the 'set' function for ConfigurationSettings:
private ConfigurationSettings _configurationSettings { get; set; }
public ConfigurationSettings ConfigurationSettings
{
get
{
return _configurationSettings;
}
set
{
if (_configurationSettings != value)
{
_configurationSettings = value;
_configurationSettings = new Model.ConfigurationSettings();
_configurationSettings.PropertyChanged += (o, args) =>
{
StartCommand.RaiseCanExecuteChanged();
};
RaisePropertyChanged("ConfigurationSettings");
}
}
}
The problem was where I was setting my Data Context which I did not originally suspect was at all the problem. I load the view model from an XML file on disk. And when the application is closed, I overwrite that file with the latest ViewModel.
In the constructor I was reading and setting the DataContext:
public MainWindowView()
{
InitializeComponent();
string appPath = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().CodeBase);
DataSourcePath = new Uri(System.IO.Path.Combine(appPath, DataFileName));
if (File.Exists(DataSourcePath.LocalPath))
{
XmlReader reader = XmlReader.Create(DataSourcePath.LocalPath);
DataContext = (MainWindowViewModel)serialize.Deserialize(reader);
reader.Close();
}
else
{
WriteDataViewModelToDisk(); // new empty view model written to disk
}
}
If this was the first time I ran the code, with no pre-existing file, my delegate event handler actually worked. The issue was when this code loaded a pre-existing XML file, it overwrote the ConfigurationSettings property in my view model - thus destroying the event subscription.

Related

How to change the main view from the "Content" view

I have an application that look like this
The whole window is defined in the MainWindow.xaml, the green part is the content control
<ContentControl Grid.Row="1"
Grid.Column="1"
Margin="5"
Content="{Binding CurrentView}"/>
The MainViewModel looks like this:
public RelayCommand HomeViewCommand { get; set; }
public RelayCommand DetailsViewCommand { get; set; }
public HomeViewModel HomeVm { get; set; }
public DetailsViewModel DetailsVm { get; set; }
private object _currentView;
public object CurrentView
{
get { return _currentView; }
set
{
_currentView = value;
OnPropertyChanged();
}
}
public MainViewModel()
{
HomeVm = new HomeViewModel();
DetailsVm = new DetailsViewModel();
CurrentView = HomeVm;
HomeViewCommand = new RelayCommand(o =>
{
CurrentView = HomeVm;
});
}
Current and default content of the MainView is the HomeView, I already implemented the event trigger on pressing the item in the list in the HomeView. I want to know, what should I write in the HomeView method (which is triggering on the click on the item) in order to change the MainView content part to another View (DetailsView in my case). Code in my HomeViewModel:
private void RaisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
{
//something here to change the currentView of the MainViewModel
}
}
Got an answer from the guy from discord server.https://discord.gg/AvnpSMDY
You could pass the MainViewModel into the constructor of another view model and assign it to a private readonly field like _mainViewModel. This way you can change the current view either by changing the CurrentView property:
_mainViewModel.CurrentView = _mainViewModel.DetailsVm;
or executing commands:
_mainViewModel.DetailsViewCommand.Execute(null);

ListBox bind to ObservableCollection is not updated with collection

I have next model:
public class MyModel
{
public ObservableCollection<MyObject> MyList {get; set;}
}
public class MyObject
{
MyObservableDictionary MyDictionary {get; set;}
}
public class MyObservableDictionary : ObservableCollection<EnymValue>
{
}
public class EnymValue : INotifyPropertyChanged
{
private MyEnum key;
private string value;
public MyEnum Key
{
get
{
return this.key;
}
set
{
this.key = value;
NotifyPropertyChanged("Key");
}
}
public string Value
{
get
{
return this.value;
}
set
{
this.value = value;
NotifyPropertyChanged("Value");
}
}
public LanguageValue(MyEnum key, string value)
{
this.Key = key;
this.Value = value;
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged([System.Runtime.CompilerServices.CallerMemberName]string propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
public enum MyEnum
{
}
And on View I have a ListBox:
<ListBox x:Name="MyList" SelectionMode="Single" ItemsSource="{Binding Path=MyList, Mode=OneWay}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=MyDictionary, Mode=OneWay, Converter={StaticResource myEnumToTextConverter}}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
(myEnumToTextConverter converter is just selects first element from collection and return it's value, or some specified constant if collection is null or empty)
I want my Model's list box to be updated on view, when any EnymValue values are changed.
Is it possible somehow to implement this?
Currently the view is not updated when Value changed.
I've tried to inherit EnymValue from INotifyPropertyChanged, but this didn't helped. Looks like PropertyChanged == null on EnymValue.NotifyPropertyChanged when property updated.
ObservableCollection is able to notify UI about changes when collection itself is changed(elemends are added or deleted). But ObservableCollection is not aware of changes that are happening when you modify one of it's items. To solve the problem you may subscribe to CollectionChange event of observable collection, and when new item is added, subscribe to new items's PropertyChanged. When PropertyChanged event is raised, you can trigger notification on your list OnPropertyChanged(()=>MyItems); You should be careful implementing this solution and remember to unsubscribe from the event's to avoid memory leaks.
An example of what I mean you can see in this answer.
Your MyDictionary should force a refresh. Easiest way is to re-assign its old value, and implement INPC in MyObject like below :
public class MyObject: INotifyPropertyChanged
{
MyObservableDictionary _myDictionary;
public MyObservableDictionary MyDictionary {
get
{
return _myDictionary;
}
set
{
_myDictionary = value;
OnPropertyChanged("MyDictionary");
}
}
public MyObject()
{
MyDictionary = new MyObservableDictionary();
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string prop)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(prop));
}
}
Sample code to change Value :
private void Button_Click(object sender, RoutedEventArgs e)
{
// vm is ViewModel instance, vm is DataContext set for Window
var old = vm.MyList[0].MyDictionary;
vm.MyList[0].MyDictionary[0].Value = "aaaa";
vm.MyList[0].MyDictionary = old;
}
I tested this, and it displays changed value as "aaaa".

WPF bind Textbox to a DataSet in another class

WPF is not normally my area, so I am a bit of a newbie, and I am having a bit of trouble figuring out how to achieve something in WPF which was a piece of cake in WinForms. I can't seem to find either the right thread in this forum or the right YouTube tutorial that leads me towards the answer. I am having problems getting a simple DataBinding to a WPF TextBox working correctly. The behaviour that I am trying to achieve is that any changes made to the TextBox are immediately reflected in the source class DataSet. It's a simple display/edit scenario and I'm sure there is a very simple answer.
This is how I would have done it in WinForms....
Form code:
public partial class Form1 : Form
{
private DATARECORD CURRENTUSER;
public Form1()
{
InitializeComponent();
CURRENTUSER = new DATARECORD(#"Data Source=C:\Users\rr187718\Documents\Personal\Programming\DynamicBackup\DynamicBackup\bin\Debug\Data\dbData.sdf");
CURRENTUSER.FncBind(CtlCopiesToKeep, "Value", "tblUser.CopiesToKeep");
}
//Test code to display the value in the DataSet
private void button1_Click(object sender, EventArgs e)
{
MessageBox.Show(CURRENTUSER.copiesToKeep.ToString());
}
}
Class code:
public class DATARECORD
{
private string ConnectionString;
private DataSet CurrentRecord;
public int copiesToKeep { get { return Int32.Parse(CurrentRecord.Tables["tblUser"].Rows[0]["CopiesToKeep"].ToString()); } }
public DATARECORD(string connectionString)
{
ConnectionString = connectionString;
CurrentRecord = new DataSet();
SQL SQL = new SQL(2);
DataTable userTable = SQL.fncSelectAsTable(ConnectionString, "tblUser", "USERID=2");
userTable.TableName = "tblUser";
CurrentRecord.Tables.Add(userTable);
userTable.Dispose();
}
public void FncBind(Control c, string type, string field)
{
c.DataBindings.Add(type, CurrentRecord, field, true, DataSourceUpdateMode.OnPropertyChanged);
}
}
I then just have simple TextBox on the main Form called "CtlCopiesToKeep" and a "test" button.
Does anyone know of a nice, simple, example that can show how to do this?
Many thanks in advance,
Dave
EDIT:
Hello Noel. Many thanks for taking the time to explain all that. I have put it altogether, but something seems to be wrong with the binding, because when I change the value in the TextBox it does not update the DataSet. Here is the code and the XAML. If anyone can point me in the right direction then it would be much appreciated.
UPDATED Main code
public partial class MainWindow : Window
{
public DATARECORD SELECTEDUSER;
private string ConnectionString = #"Data Source=C:\Users\rr187718\Documents\Personal\Programming\DynamicBackup\DynamicBackup\bin\Debug\Data\dbData.sdf";
public MainWindow()
{
InitializeComponent();
SELECTEDUSER = new DATARECORD(ConnectionString);
GrdMain.DataContext = SELECTEDUSER;
}
private void button1_Click(object sender, RoutedEventArgs e)
{
SELECTEDUSER.fncShowVals("BasePath");
}
}
UPDATED Class code
public class DATARECORD : INotifyPropertyChanged
{
private string ConnectionString;
private DataSet currentRecord = new DataSet();
private string BasePath = null;
public string basePath
{
get
{
return currentRecord.Tables["tblStorage"].Rows[0]["BasePath"].ToString() ;
}
set
{
BasePath = value;
OnPropertyChanged("BasePath");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public DATARECORD(string connectionString)
{
ConnectionString = connectionString;
SQL SQL = new SQL(ConnectionString, SQLVersion.CE);
DataTable storageTable = SQL.fncSelectAsTable(ConnectionString, "tblStorage", "USERID=2");
storageTable.TableName = "tblStorage";
currentRecord.Tables.Add(storageTable);
storageTable.Dispose();
}
public void fncShowVals(string test)
{
MessageBox.Show(currentRecord.Tables["tblStorage"].Rows[0][test].ToString());
}
protected void OnPropertyChanged(string value)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(value));
}
}
}
XAML for TextBox
<Window x:Class="WpfBind.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid Name="GrdMain">
<TextBox Text="{Binding basePath, Mode=TwoWay, UpdateSourceTrigger =PropertyChanged}" Height="23" HorizontalAlignment="Left" Margin="124,70,0,0" Name="CtlBaseFolder" VerticalAlignment="Top" Width="120" />
<Label Content="BaseFolder" Height="28" HorizontalAlignment="Left" Margin="41,69,0,0" Name="label2" VerticalAlignment="Top" />
<Button Content="Button" Height="23" HorizontalAlignment="Left" Margin="263,142,0,0" Name="button1" VerticalAlignment="Top" Width="75" Click="button1_Click" />
</Grid>
UPDATE 02/04/2015
I now have this, but I don't understand how it references the DataSet? This code produces a blank textbox and if the value is changed it doesn't update the DataSet:
`private string ___basePath = null;
protected string _basePath
{
get
{
return ___basePath;
}
set
{
___basePath = value;
OnPropertyChanged("basePath");
}
}
public string basePath
{ //<- Bind to this property
get
{
return ___basePath;
}
set
{
_basePath = value;
}
}`
The underlying DataSet value is stored here:
currentRecord.Tables["tblStorage"].Rows[0]["BasePath"].ToString();
Many thanks in advance, Dave.
UPDATE - 02/04/2015 - 2
Hello Noel, I have applied your code, but it's still not working unfortunately (the DataSet does not reflect the changes in the TextBox if I click on the "test" button). Here is the whole code. I massively appreciate your time on this by the way, thanks so much!
public partial class MainWindow : Window
{
private string ConnectionString = #"Data Source=C:\Users\rr187718\Documents\Personal\Programming\DynamicBackup\DynamicBackup\bin\Debug\Data\dbData.sdf";
private readonly DATARECORD _data = null;
public DATARECORD Data
{
get
{
return _data;
}
}
public MainWindow()
{
InitializeComponent();
_data = new DATARECORD(ConnectionString);
DataContext = Data; //All controls connected to this class will now look for their value in 'Data' (DataContext inherits and must be a property because you can only bind to properties)
}
private void button1_Click(object sender, RoutedEventArgs e)
{
Data.fncShowVals("BasePath");
}
}
public class DATARECORD : INotifyPropertyChanged
{
private string ConnectionString;
private DataSet currentRecord = new DataSet();
private string ___basePath = null;
private string _basePath
{
get
{
if (___basePath == null)
{
//We only access the currentRecord if we did not yet stored the value
// otherwise it would read the currentRecord every time you type a char
// in the textbox.
// Also: Pay attention to multiple possible NullReferenceExceptions and IndexOutOfBoundsExceptions
___basePath = currentRecord.Tables["tblStorage"].Rows[0]["BasePath"].ToString();
}
return (___basePath == String.Empty) ? null : ___basePath;
}
set
{
___basePath = (value == null) ? String.Empty : value;
NotifyPropertyChanged("BasePath");
}
}
protected void PushBasePathToDataBase()
{
//Save the value of ___basePath to the database
}
public string BasePath
{ //The Binding recieves/sets the Data from/to this property
get
{
return _basePath;
}
set
{
_basePath = value;
}
}
public event PropertyChangedEventHandler PropertyChanged;
public DATARECORD(string connectionString)
{
ConnectionString = connectionString;
SQL SQL = new SQL(ConnectionString, SQLVersion.CE);
DataTable storageTable = SQL.fncSelectAsTable(ConnectionString, "tblStorage", "USERID=2");
storageTable.TableName = "tblStorage";
currentRecord.Tables.Add(storageTable);
storageTable.Dispose();
___basePath = currentRecord.Tables["tblStorage"].Rows[0]["BasePath"].ToString();
}
public void fncShowVals(string test)
{
MessageBox.Show(currentRecord.Tables["tblStorage"].Rows[0][test].ToString());
}
protected void NotifyPropertyChanged(string PropertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
}
}
It is great that you are using a binding to seperate data from the visuals. Since that was not really possible in winforms. In order for a binding to work you must do the following:
The textBox must have its DataContext set to the instance of a class which holds the binding-value. DataContext = MyDataInstance; You can set that on the textbox itself or on any parent.
The value as well as the DataContext you want to bind must be a public property. F.e:
private string _name = null;
public string Name{
get{
return _name;
}
set{
_name = value;
NotifyPropertyChanged("Name");
}
}
The Data Class must implement INotifyPropertyChanged
If that is all set up you can write your textbox in xaml:
<TextBlock Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
This Binding binds to the Name property of the instance specified in DataContext. It can retrieve the value from the property and it can write the data to it.
It recieves Data when you call NotifyPropertyChanged("Name"); in your DataClass
It writes Data when the property of the control changes (Requires Mode set to TwoWay and the UpdateSourceTrigger to PropertyChanged)
EDIT (regarding your additional content)
I noticed that you wanted to notify about your private field named "BasePath".
You must notify the property "basePath" and not the field behind it.
That is why I recommend a strict naming convention.
I do name private and protected fields like _privateOrProtected (1 underscore).
I name private or protected fields accessed by bindings properties like ___someData (3 underscores) and the binding property like SomeData. The reason is, that you usually don't want to set the private field directly except from the binding propertie's setter. Setting it directly would not call the NotifyPropertyChanged(); which obviously isn't what you want in almost all cases. And if you keep the 3 underscores throughout your app - everyone familliar with bindings should quickly understand the meaning.
For more complex data you might have a binding property accessing a private/protected property accessing a private field. I would solve it like this: SomeData, _someData, ___someData. You just have to make it clear wich properties or fields can be set in order to update the binding otherwise someone might change the value of ___someData and wonder why the binding isn't updating.
Since this is a quite important point in every WPF app I really want you to understand it. Here is an example for the stuff above:
private bool ___thisIsAwesome = true;
protected bool _thisIsAwesome{
get{
return ___thisIsAwesome;
}
set{
___thisIsAwesome = value;
NotifyPropertyChanged("ThisIsAwesome");
}
}
public bool ThisIsAwesome{ //<- Bind to this property
get{
return ___thisIsAwesome;
}
/*set{
_thisIsAwesome = value;
} NOTE: The setter is not accessable from outside of this class
because nobody can tell me that this is not awesome - it just is.
However I still want to be able to set the property correctly
from within my class (in case I change my mind), that is why I
added the protected property.
If you omit a getter/setter like this one make sure your
<br>Binding Mode</b> does not try to access the omited accessors.
Also check the output window too find possible binding errors
which never throw exceptions.
*/
}
In this code you should now recognize that setting ThisIsAwesome and _thisIsAwesome will both update the binding. But beware of setting ___thisIsAwesome because it won't update the Binding. The setter of ThisIsAwesome is currently not available (whatever reason) and that's why I added the protected property. Do you understand what I want to achieve with that?
EDIT2 (because your code still doesn't work)
public partial class MainWindow : Window {
private readonly MyData _data = null;
public MyData Data{
get{
return _data;
}
}
public MainWindow() {
_data = new MyData();
DataContext = Data; //All controls connected to this class will now look for their value in 'Data' (DataContext inherits and must be a property because you can only bind to properties)
}
}
public class MyData : INotifyPropertyChanged {
private string ___basePath = null;
private string _basePath {
get {
if (___basePath == null) {
//We only access the currentRecord if we did not yet stored the value
// otherwise it would read the currentRecord every time you type a char
// in the textbox.
// Also: Pay attention to multiple possible NullReferenceExceptions and IndexOutOfBoundsExceptions
___basePath = currentRecord.Tables["tblStorage"].Rows[0]["BasePath"].ToString();
}
return (___basePath == String.Empty) ? null : ___basePath;
}
set {
___basePath = (value == null) ? String.Empty : value;
NotifyPropertyChanged("BasePath");
}
}
protected void PushBasePathToDataBase() {
//Save the value of ___basePath to the database
}
public string BasePath{ //The Binding recieves/sets the Data from/to this property
get{
return _basePath;
}
set{
_basePath = value;
}
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string PropertyName){
if(PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
}
#endregion INotifyPropertyChanged
}
And finally the textbox in your MainWindow's xaml:
<TextBlock Text="{Binding BasePath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>

How to update ListView when existent items of bound list has been updated

I need to update the list of downloads when the progress has been changed.
XAML:
<ListView ItemsSource="{Binding UiList}" x:Name="MyListView">
<ListView.View>
<GridView>
<GridViewColumn Header="Title"/>
<GridViewColumn Header="Progress"/>
</GridView>
</ListView.View>
</ListView>
ViewModel that creates an instance of Logic class and updates the content of ListView:
class MainWindowViewModel : ViewModelBase
{
private readonly Logic _logic;
public List<Model> UiList { get; set; }
public MainWindowViewModel()
{
_logic = new Logic();
_logic.Update += LogicUpdate;
Start = new RelayCommand(() =>
{
var worker = new BackgroundWorker();
worker.DoWork += (sender, args) => _logic.Start();
worker.RunWorkerAsync();
});
}
void LogicUpdate(object sender, EventArgs e)
{
UiList = _logic.List;
RaisePropertyChanged("UiList");
}
public ICommand Start { get; set; }
}
Logic:
public class Logic
{
readonly List<Model> _list = new List<Model>();
public event EventHandler Update;
public List<Model> List
{
get { return _list; }
}
public void Start()
{
for (int i = 0; i < 100; i++)
{
_list.Clear();
_list.Add(new Model{Progress = i, Title = "title1"});
_list.Add(new Model { Progress = i, Title = "title2" });
var time = DateTime.Now.AddSeconds(2);
while (time > DateTime.Now)
{ }
Update(this, EventArgs.Empty);
}
}
}
The code above would not update UI. I know two way how to fix this:
In xaml codebehind call: Application.Current.Dispatcher.Invoke(new Action(() => MyListView.Items.Refresh()));
In ViewModel change List<> to ICollectionView and use Application.Current.Dispatcher.Invoke(new Action(() => UiList.Refresh())); after the list has been updated.
Both ways cause the problem: the ListView blinks and Popup that should be open on user demand always closes after each "refresh":
<Popup Name="Menu" StaysOpen="False">
I can replace the popup with another control or panel, but I need its possibility to be out of main window's border (like on screen). But I believe that WPF has another way to update the content of ListView (without blinks).
PS: Sorry for long question, but I do not know how to describe it more briefly...
I think the reason this line doesn't work:
RaisePropertyChanged("UiList");
Is because you haven't actually changed the list. You cleared it and repopulated it, but it's still the reference to the same list. I'd be interested to see what happens if, instead of clearing your list and repopulating, you actually created a new list. I think that should update your ListView as you expected. Whether or not it has an effect on your popup, I don't know.
I've found the answer here: How do I update an existing element of an ObservableCollection?
ObservableCollection is a partial solution. ObservableCollection rises CollectionChanged event only when collection changes (items added, removed, etc.) To support updates of existent items, each object inside the collection (Model class in my case) must implement the INotifyPropertyChanged interface.
// I used this parent (ViewModelBase) for quick testing because it implements INotifyPropertyChanged
public class Model : ViewModelBase
{
private int _progress;
public int Progress
{
get { return _progress; }
set
{
_progress = value;
RaisePropertyChanged("Progress");
}
}
public string Title { get; set; }
}

Data bind to a portion of a collection

I have a static collection of items (say numbers from 1 to 100) that I'm presenting in a ComboBox. I can bind these items to the ComboBox no problem.
My question is how to bind a second ComboBox to a subset of those items. The behavior I want is to have the second ComboBox bound to the subset of items remaining after the first ComboBox is selected. For example the first ComboBox would show 1,2,3...,100. If the number 43 is selected in the first ComboBox then the second ComboBox should show 44,45,...,100.
How can this be accomplished and have the second ComboBox update if the first is changed without a lot of code-behind?
I would do this with a using MVVM pattern.
Create a class that implements INotifyChange and expose three Property.
ICollection FullCollection
int FirstIndex
ICollection PartialCollection
Use this class as DataContext for your Control and bind SelectedIndex of the first combo box to FirstIndex property, ItemSource of first combobox to FullCollection and ItemSource of second collection to PartialCollection (Be sure that SelectedIndex binding mode is Two Way).
Then on set of FirstIndex property set the PartialCollection property as you want.
Remember that you have to use NotifyPropertyChange on set method of each Properties.
Hope this help.
I'd go with MVVM design but if you want to just do testing and want to see quick results without going into design patterns then I'd suggest using something like CLINQ / BLINQ / Obtics framework to leverege the power of LINQ while keeping the results live for the combo box.
Since LoSciamano has already posted a reply on that (while i was posting this!) I won't dive into details of MVVM implementation
I would expose the first collection as an ObservableCollection<T> and the second collection as a bare IEnumerable. You can then use the default collection view to handle the events necessary to re-filter the sub collection.
class FilteredVM : ViewModelBase
{
public ObservableCollection<MyVM> Items { get; private set; }
public IEnumerable<MyVM> SubItems { get; private set; }
public FilteredVM()
{
this.Items = new ObservableCollection<MyVM>();
this.SubItems = Enumerable.Empty<MyVM>();
var view = CollectionViewSource.GetDefaultView(this.Items);
view.CurrentChanged += (sender, e) => { SetupFilter(); };
view.CollectionChanged += (sender, e) => { SetupFilter(); };
}
private void SetupFilter()
{
var view = CollectionViewSource.GetDefaultView(this.Items);
var current = view.CurrentItem;
if (current != null)
{
this.SubItems = this.Items.Where((vm,idx) => idx > view.CurrentPosition);
}
else
{
this.SubItems = Enumerable.Empty<MyVM>();
}
this.OnPropertyChanged("SubItems");
}
}
Alternatively, if you'd like to keep CollectionViewSource out of your VM:
class FilteredVM : ViewModelBase
{
private MyVM selectedItem;
public MyVM SelectedItem
{
get { return this.selectedItem; }
set
{
if (value != this.selectedItem)
{
this.selectedItem = value;
this.OnPropertyChanged("SelectedItem");
this.SetupFilter();
}
}
}
public ObservableCollection<MyVM> Items { get; private set; }
public IEnumerable<MyVM> SubItems { get; private set; }
public FilteredVM()
{
this.Items = new ObservableCollection<MyVM>();
this.SubItems = Enumerable.Empty<MyVM>();
this.Items.CollectionChanged += (sender, e) => { this.SetupFilter(); };
}
private void SetupFilter()
{
if (this.SelectedItem != null)
{
var item = this.SelectedItem; // save for closure
this.SubItems = this.Items.SkipWhile(vm => vm != item).Skip(1);
}
else
{
this.SubItems = Enumerable.Empty<MyVM>();
}
this.OnPropertyChanged("SubItems");
}
}
Keep in mind this will require SelectedItem to be properly bound in the View to the ViewModel. The first approach listed allows the SelectedItem to be bound to anywhere (or nowhere).
Have 2 Observable collections so that when item in the 1st box is selected, it will kick off method which clears and re populates the 2nd collection. As it is observableCollection, it gets reflected in the WPF GUI automatically
Here is a concrete example (as usual, may be improved, but the idea is here) :
Code behind an view model :
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
DataContext = new ViewModel();
}
}
public class ViewModel : INotifyPropertyChanged
{
public ViewModel()
{
Initialsource = new ObservableCollection<int>();
for (int i = 0; i < 101; i++)
{
Initialsource.Add(i);
}
}
private int _selectedsourceItem;
public int SelectedsourceItem
{
get { return _selectedsourceItem; }
set
{
_selectedsourceItem = value;
SubsetSource = new ObservableCollection<int>(Initialsource.Where(p => p > _selectedsourceItem));
InvokePropertyChanged(new PropertyChangedEventArgs("SubsetSource"));
InvokePropertyChanged(new PropertyChangedEventArgs("SelectedsourceItem"));
}
}
private ObservableCollection<int> _initialsource;
public ObservableCollection<int> Initialsource
{
get { return _initialsource; }
set
{
_initialsource = value;
InvokePropertyChanged(new PropertyChangedEventArgs("Initialsource"));
}
}
private ObservableCollection<int> _subsetSource;
public ObservableCollection<int> SubsetSource
{
get { return _subsetSource ?? (_subsetSource = new ObservableCollection<int>()); }
set
{
_subsetSource = value;
InvokePropertyChanged(new PropertyChangedEventArgs("SubsetSource"));
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void InvokePropertyChanged(PropertyChangedEventArgs e)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, e);
}
}
XAML :
<Grid HorizontalAlignment="Center" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal">
<ComboBox Width="100" ItemsSource="{Binding Initialsource}" SelectedItem="{Binding SelectedsourceItem, Mode=TwoWay}"></ComboBox>
<ComboBox Width="100" ItemsSource="{Binding SubsetSource}"></ComboBox>
</StackPanel>
</Grid>
You can use my ObservableComputations library:
ObservableCollection<Item> itemsSubset = ItemsObservableCollection
.Skiping(fromIndex)
.Taking(itemsInSubsetCount);
itemsSubset reflacts all the changes in ItemsObservableCollection.

Resources