I have created a class Track which represents a song in the playlist:
public class Track
{
public Uri Path
{
get { return path; }
set { path = value; }
}
public TrackState State
{
get { return state; }
set { state = value; }
}
private Uri path;
private TrackState state;
}
Next I have created MainWindowController class which interacts between the UI window and the Track class:
public class MainWindowController : INotifyPropertyChanged
{
public ObservableCollection<Track> Playlist
{
get { return playlist; }
set
{
if (value != this.playlist)
{
playlist = value;
NotifyPropertyChanged("Playlist");
}
}
}
public int NowPlayingTrackIndex
{
set
{
if (value >= 0)
{
playlist[nowPlayingTrackIndex].State = TrackState.Played;
playlist[value].State = TrackState.NowPlaying;
this.nowPlayingTrackIndex = value;
}
}
}
private ObservableCollection<Track> playlist;
private int nowPlayingTrackIndex;
}
Basically, this class stores playlist collection and an index of the currently played track. Lastly, I have created the UI window in WPF:
<Window ...>
...
<ListBox
Name="PlaylistListBox"
ItemsSource="{Binding Source={StaticResource ResourceKey=PlaylistViewSource}}"
ItemTemplateSelector="{Binding Source={StaticResource ResourceKey=TrackTemplateSelector}}"
MouseDoubleClick="PlaylistListBox_MouseDoubleClick" />
...
</Window>
and the corresponding code behind:
...
private void PlaylistListBox_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
int index = this.PlaylistListBox.SelectedIndex;
this.windowController.NowPlayingTrackIndex = index;
}
...
Items source points to the static resource where the CollectionViewSource is defined. The ItemTemplateSelector defines which DataTemplate to use for list box items depending on the track state (NowPlaying or Played).
When the user double-clicks the playlist item, the NowPlayingTrackIndex in the MainWindowController gets updated and it updates the track state. The problem is, the DataTemplates for list box items do not get updated on the window, i.e. the the double-clicked list box item does not change the data template. Why?
I tried setting the PropertyChanged for track state but it didn't help. What am I missing? Thank you.
There are two issues to be addressed in your code.
First, you should know that ObservableCollection notify its observers about changes to its own elements, it doesn't know or care about changes to the properties of its elements. In other words, it doesn't watch for property change notification on the items within its collection. So changing the Track object property value in the PlayList collection doesn't watch by any mean. Here is an article about the subject.
Second, your MainWindowController doesn't broadcast for NowPlayingTrackIndex property value change at all. You should call NotifyPropertyChanged("NowPlayingTrackIndex") to notify interesting parties about the change of the current playing track. This may solve your problem but more elegant way, and my suggestion, would be implementing a custom ObservableCollection class (something like TrackObservableCollection) that contains NowPlaying property rather than implementing it in the MainWindowController class which looks like an unnecessary intermediation.
Related
I am stuked with the following situation:
I define an ObservableColletion:
public ObservableCollection<Model.OSModel> OS { get; private set; }
and instantiate it in the contructor:
public MyOSViewModel() // Constructor
{
OS = new ObservableCollection<Model.OSModel>();
}
When added a item to the collection OS:
public void OnTabClicked(ListaServicosTab listaServicosTab)
{
OS.Add(listaServicosTab.vm.OS);
OnPropertyChanged("OS");
}
it doesn't binding do TextBox.
But, if a instantiate the collection inside a method:
public void OnTabClicked(ListaServicosTab listaServicosTab)
{
OS = new ObservableCollection<Model.OSModel>();
OS.Add(listaServicosTab.vm.OS);
OnPropertyChanged("OS");
}
It Works fine.
Anyone can tell me why that is happening?
My Xaml Script:
<DockPanel Background="CadetBlue" DataContext="{StaticResource OSData}">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock Text="{Binding Path=Nome}" FontFamily="Calibri" FontSize="20"/>
</StackPanel>
</DockPanel>
You need to notify the collection changed event of ObservableCollection.
public ObservableCollection<obj> Notifications
{
get { return _Notifications; }
set
{
_Notifications = value;
NotifyPropertyChanged();
Notifications.CollectionChanged -= Notifications_CollectionChanged;
Notifications.CollectionChanged += Notifications_CollectionChanged;
}
}
}
void Notifications_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
//Notify
}
MSDN
ObservableCollection represents a dynamic data collection that provides notifications when items get added, removed, or when the whole list is refreshed.
You dont need at all OnPropertyChanged("OS"); you are not replacing OS object instead you are just adding or removing stuff.
The difference between List and Observable collection is that OS.Add() is going to trigger the OnPropertyChanged for the "item" in the collection. In List it will not happen. Thats why we use an observable collection which will do all the work for you.
public void OnTabClicked(ListaServicosTab listaServicosTab)
{
OS = new ObservableCollection<Model.OSModel>();// Dont do this
OS.Add(listaServicosTab.vm.OS);
OnPropertyChanged("OS");// Dont do this
}
Everytime you click you are replacing the observable collection. You will end up having only one object.
public MyOSViewModel() // Constructor
{
OS = new ObservableCollection<Model.OSModel>();
// should initialize only in constructor
//not in a place it will be called multiple times
}
public void OnTabClicked(ListaServicosTab listaServicosTab)
{
OS.Add(listaServicosTab.vm.OS);
}
In the XAML you should have an Items Control or a List to bind this collection not stack panel.
I have model:
public class Song
{
public int ContentID { get; set; }
public bool IsSelected
{
get
{
var song = PlayerHelper.ReadNowPlaying();
return song.Id == ContentID;
}
}
}
I have a view with ListBox:
<ListBox x:Name="songsLstBox" ItemsSource="{Binding Top100Songs}" />
And ViewModel with list of Songs items. So, sometimes i want to refresh (redraw) the listbox. It's need to display that IsSelected is changed (No, i can't using INotifyPropertyChanged in model and setting it in viewmodel).
So how i can redraw a listbox in WP7? I can't find any Refresh or Update method for UIElements.
I tried calling this.OnPropertyChanged("Top100Songs"); but it doesn't work. I tried calling UpdateLayout - the same.
One way is setting DataContex for page to null and then revert to my ViewModel. It works, but is so long (about 5 secs. for changing).
So any ideas?
Write your own collection wrapper and use it for the Top100Songs property
class SongCollection : ObservableCollection<Song>
{
public Refresh()
{
OnCollectionChanged(
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
Short Version
If I update the Model object that my ViewModel wraps, what's a good way to fire property-change notifications for all the model's properties that my ViewModel exposes?
Detailed Version
I'm developing a WPF client following the MVVM pattern, and am attempting to handle incoming updates, from a service, to data being displayed in my Views. When the client receives an update, the update appears in the form of a DTO which I use as a Model.
If this model is an update to an existing model being shown in the View, I want the associated ViewModel to update its databound properties so that the View reflects the changes.
Let me illustrate with an example. Consider my Model:
class FooModel
{
public int FooModelProperty { get; set; }
}
Wrapped in a ViewModel:
class FooViewModel
{
private FooModel _model;
public FooModel Model
{
get { return _model; }
set
{
_model = value;
OnPropertyChanged("Model");
}
}
public int FooViewModelProperty
{
get { return Model.FooModelProperty; }
set
{
Model.FooModelProperty = value;
OnPropertyChanged("FooViewModelProperty");
}
}
The Problem:
When an updated model arrives, I set the ViewModel's Model property, like so:
instanceOfFooVM.Model = newModel;
This causes OnPropertyChanged("Model") to fire, but not OnPropertyChanged("FooViewModelProperty"), unless I call the latter explicitly from Model's setter. So a View bound to FooViewModelProperty won't update to display that property's new value when I change the Model.
Explicitly calling OnPropertyChanged for every exposed Model property is obviously not a desirable solution, and neither is taking the newModel and iterating through its properties to update the ViewModel's properties one-by-one.
What's a better approach to this problem of updating a whole model and needing to fire change notifications for all its exposed properties?
According to the docs:
The PropertyChanged event can indicate all properties on the object have changed by using either null or String.Empty as the property name in the PropertyChangedEventArgs.
One option is to listen to your own events, and make a helper routine to raise the other notifications as required.
This can be as simple as adding, in your constructor:
public FooViewModel()
{
this.PropertyChanged += (o,e) =>
{
if (e.PropertyName == "Model")
{
OnPropertyChanged("FooViewModelProperty");
// Add other properties "dependent" on Model here...
}
};
}
Whenever your Model property is set, subscribe to its own PropertyChanged event. When your handler gets called, fire off your own PropertyChanged event. When the Model is set to something else, remove your handler from the old Model.
Example:
class FooViewModel
{
private FooModel _model;
public FooModel Model
{
get { return _model; }
set
{
if (_model != null)
{
_model.PropertyChanged -= ModelPropertyChanged;
}
if (value != null)
{
value.PropertyChanged += ModelPropertyChanged;
}
_model = value;
OnPropertyChanged("Model");
}
}
public int FooViewModelProperty
{
get { return Model.FooModelProperty; }
set
{
Model.FooModelProperty = value;
OnPropertyChanged("FooViewModelProperty");
}
}
private void ModelPropertyChanged(object sender, PropertyChangedEventArgs e)
{
// Here you will need to translate the property names from those
// present on your Model to those present on your ViewModel.
// For example:
OnPropertyChanged(e.PropertyName.Replace("FooModel", "FooViewModel"));
}
}
Implements INotifyPropertyChanged
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(String.Empty))
For VB.net if anybody else needs it. If you have already implemented "INotifyPropertyChanged" then the last line is all you need.
I have an INotifyProperty Screen item that I have bound to a wpf control.
Ok... I Simplified everything and am posting more code. I have a MainViewModel with the selected screen property.
public Screen SelectedScreen
{
get { return this.selectedScreen; }
set
{
this.selectedScreen = value;
this.OnPropertyChanged("SelectedScreen");
}
}
I have a textbox that is bound to this property:
<TextBlock Text="{Binding Path=SelectedScreen.ScreenNumber}" />
This all works initially. I have created another control that is changing the selected screen with the following code.
public Screen SelectedScreen
{
get { return (Screen)GetValue(SelectedScreenProperty); }
set
{
this.SetValue(SelectedScreenProperty, value);
for (int x = 0; x < this.Screens.Count; ++x)
this.Screens[x].IsSelected = false;
value.IsSelected = true;
}
}
public ObservableCollection<Screen> Screens
{
get { return (ObservableCollection<Screen>)GetValue(ScreensProperty); }
set { this.SetValue(ScreensProperty, value); }
}
public static readonly DependencyProperty SelectedScreenProperty =
DependencyProperty.Register("SelectedScreen",
typeof(Screen),
typeof(ScreenSelection));
public static readonly DependencyProperty ScreensProperty =
DependencyProperty.Register("Screens",
typeof(ObservableCollection<Screen>),
typeof(ScreenSelection),
new UIPropertyMetadata(new ObservableCollection<Screen>()));
This screen selection control is working. When I change screens and put a breakpoint on the set property of SelectedScreen it is called which then calls the SelectedScreen property of the MainViewModel. So the event is firing, but the textbox isn't updated even though it binds correctly the first time.
Does the class which contains the SelectedScreen property implement INotifyPropertyChanged? When the SelectedScreen property changes, the containing class should raise the PropertyChanged event, and typically, WPF should update the Binding.
Thank you gehho for looking at this. I figured it out and there is no way you had enough information to be able too. I was inheriting from ViewModelBase in the MainViewModel that was inheriting from ObservableObject where I implemented INotifyPropertyChanged. The problem is that I implemented the methods for INotifyPropertyChanged in both classes and WPF was listening to the wrong one. Very obscure. Very annoying. Very lasjkdf;ashdoh
I have a listbox defined in XAML as:
<ListBox x:Name="directoryList"
MinHeight="100"
Grid.Row="0"
ItemsSource="{Binding Path=SelectedDirectories}"/>
The SelectedDirectories is a property on the lists DataContext of type List<DirectoryInfo>
The class which is the datacontext for the listbox implements INotifyPropertyChanged. When the collection changes the items are added successfully to the list however the display does not update until I force the listbox to redraw by resizing it.
Any ideas why?
EDIT: INotifyPropertyChanged implementation
public class FileScannerPresenter : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private FileScanner _FileScanner;
public FileScannerPresenter()
{
this._FileScanner = new FileScanner();
}
public List<DirectoryInfo> SelectedDirectories
{
get
{
return _FileScanner.Directories;
}
}
public void AddDirectory(string path)
{
this._FileScanner.AddDirectory(path);
OnPropertyChanged("SelectedDirectories");
}
public void OnPropertyChanged(string property)
{
if (this.PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
Try
ObservableCollection<DirectoryInfo>
instead - you're triggering a refresh of the entire ListBox for no reason, and you don't need to make your hosting class implement INotifyPropertyChanged - it could easily just be a property of the window. The key is to never set the property to a new instance. So:
class SomeWindow : Window {
public ObservableCollection<DirectoryInfo> SelectedDirectories {get; private set;}
SomeWindow() { SelectedDirectories = new ObservableCollection<DirectoryInfo>(); }
public void AddDirectory(string path) {
SelectedDirectories.Add(new DirectoryInfo(path));
}
}
If you end up using that FileScanner class, you need to implement INotifyCollectionChanged instead - that way, the ListBox knows what to add/remove dynamically.
(See Update below). WPF seems to be working alright. I put your code into a new project. The listbox updates whenever I click the button to invoke AddDirectory. You should not need any more code changes.
The problem seems to be something else.. Are there multiple threads in your UI?
I didnt have the FileScanner type. So I created a dummy as follows.
public class FileScanner
{
string _path;
public FileScanner()
{ _path = #"c:\"; }
public List<DirectoryInfo> Directories
{
get
{
return Directory.GetDirectories(_path).Select(path => new DirectoryInfo(path)).ToList();
}
}
internal void AddDirectory(string path)
{ _path = path; }
}
No changes to your FileScannerPresenter class. Or your listbox XAML. I created a Window with a DockPanel containing your listbox, a textbox and a button.
Update: Paul Betts is right. It works because I return a new list each time from the Bound property. Data binding with lists always messes me up.
With more tinkering, the easy way to do this is:
Make FileScanner#Directories return an ObservableCollection<DirectoryInfo> (which implements INotifyCollectionChanged for you). Change all signatures all the way up to return this type instead of a List<DirectoryInfo>
FileScanner and FileScannerPresenter themselves do not have to implement any INotifyXXX interface.
// in FileScanner class def
public ObservableCollection<DirectoryInfo> Directories
{
get
{ return _DirList; }
}
internal void AddDirectory(string path)
{
_path = path;
//var newItems = Directory.GetDirectories(_path).Select(thePath => new DirectoryInfo(thePath)).ToList();
//_DirList.Concat( newItems ); -- doesn't work for some reason.
foreach (var info in Directory.GetDirectories(_path).Select(thePath => new DirectoryInfo(thePath)).ToList())
{
_DirList.Add(info);
}
}