I'm facing a problem with displaying graphs filtered by ComboBox selection without having the UI lock up. The statistic filtering is quite heavy and needs to run async. This works fine all up until I try to call FilterStatisticsAsync and MonthSelectionChanged from the Property setter. Does anyone have a good tip on how to solve or work around this?
The XAML looks like this:
<ComboBox x:Name="cmbMonth"
ItemsSource="{Binding Months}"
SelectedItem="{Binding SelectedMonth }"
IsEditable="True"
IsReadOnly="True"
And the ViewModel property setter like this:
public string SelectedMonth
{
get { return _selectedMonth; }
set { SetProperty(ref _selectedMonth, value); LoadStatisticsAsync(); MonthSelectionChanged(); }
}
SetProperty derives from a base class which encapsulates INPC like this:
public event PropertyChangedEventHandler PropertyChanged = delegate { };
protected virtual void SetProperty<T>(ref T member, T value, [CallerMemberName] string propertyName = null)
{
if (Equals(member, value))
return;
member = value;
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
I would do it using this:
public class AsyncProperty<T> : INotifyPropertyChanged
{
public async Task UpdateAsync(Task<T> updateAction)
{
LastException = null;
IsUpdating = true;
try
{
Value = await updateAction.ConfigureAwait(false);
}
catch (Exception e)
{
LastException = e;
Value = default(T);
}
IsUpdating = false;
}
private T _value;
public T Value
{
get { return _value; }
set
{
if (Equals(value, _value)) return;
_value = value;
OnPropertyChanged();
}
}
private bool _isUpdating;
public bool IsUpdating
{
get { return _isUpdating; }
set
{
if (value == _isUpdating) return;
_isUpdating = value;
OnPropertyChanged();
}
}
private Exception _lastException;
public Exception LastException
{
get { return _lastException; }
set
{
if (Equals(value, _lastException)) return;
_lastException = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Definition of property
public AsyncProperty<string> SelectedMonth { get; } = new AsyncProperty<string>();
somewhere else in your code:
SelectedMonth.UpdateAsync(Task.Run(() => whateveryourbackground work is));
binding in xaml:
SelectedItem="{Binding SelectedMonth.Value }"
Note that properties should reflect a current state, instead of triggering processes which may take an indefinite amount of time. Hence the need to update the property in a different way from just assigning it.
Related
I have a WPF application using MVVM. I have the IsChecked value bound to a boolean on my model instance on my ViewModel. I also need to bind a method on the ViewModel to the Checked and Unchecked events. (This is so I can track unsaved changes and change the background to give my users visual indication of the need to save. I tried:
<CheckBox
Content="Enable"
Margin="5"
IsChecked="{Binding Enabled}"
Checked="{Binding ScheduleChanged}"
Unchecked="{Binding ScheduleChanged}"
/>
But I get a 'Provide value on 'System.Windows.Data.Binding' threw an exception.' error. Advice?
Here is the Model I am working with:
public class Schedule : IEquatable<Schedule>
{
private DateTime _scheduledStart;
private DateTime _scheduledEnd;
private bool _enabled;
private string _url;
public DateTime ScheduledStart
{
get { return _scheduledStart; }
set
{
_scheduledStart = value;
}
}
public DateTime ScheduledEnd
{
get { return _scheduledEnd; }
set
{
if(value < ScheduledStart)
{
throw new ArgumentException("Scheduled End cannot be earlier than Scheduled Start.");
}
else
{
_scheduledEnd = value;
}
}
}
public bool Enabled
{
get { return _enabled; }
set { _enabled = value; }
}
public string Url
{
get { return _url; }
set { _url = value; }
}
public bool Equals(Schedule other)
{
if(this.ScheduledStart == other.ScheduledStart && this.ScheduledEnd == other.ScheduledEnd
&& this.Enabled == other.Enabled && this.Url == other.Url)
{
return true;
}
else
{
return false;
}
}
}
My viewModel contains a property that has an ObservableCollection. An ItemsControl binds to the collection and generates a list. So my ViewModel sort of knows about my Model instance, but wouldn't know which one, I don't think.
Checked and Unchecked are events, so you can not bind to them like you can IsChecked, which is a property. On a higher level it is also probably wise for your view model not to know about a checkbox on the view.
I would create an event on the view model that fires when Enabled is changed, and you can subscribe to that and handle it any way you like.
private bool _enabled;
public bool Enabled
{
get
{
return _enabled;
}
set
{
if (_enabled != value)
{
_enabled = value;
RaisePropertyChanged("Enabled");
if (EnabledChanged != null)
{
EnabledChanged(this, EventArgs.Empty);
}
}
}
}
public event EventHandler EnabledChanged;
// constructor
public ViewModel()
{
this.EnabledChanged += This_EnabledChanged;
}
private This_EnabledChanged(object sender, EventArgs e)
{
// do stuff here
}
You should be able to just handle this in the setter for Enabled...
public class MyViewModel : ViewModelBase
{
private bool _isDirty;
private bool _enabled;
public MyViewModel()
{
SaveCommand = new RelayCommand(Save, CanSave);
}
public ICommand SaveCommand { get; }
private void Save()
{
//TODO: Add your saving logic
}
private bool CanSave()
{
return IsDirty;
}
public bool IsDirty
{
get { return _isDirty; }
private set
{
if (_isDirty != value)
{
RaisePropertyChanged();
}
}
}
public bool Enabled
{
get { return _enabled; }
set
{
if (_enabled != value)
{
_enabled = value;
IsDirty = true;
}
//Whatever code you need to raise the INotifyPropertyChanged.PropertyChanged event
RaisePropertyChanged();
}
}
}
You're getting a binding error because you can't bind a control event directly to a method call.
Edit: Added a more complete example.
The example uses the MVVM Lite framework, but the approach should work with any MVVM implementation.
I have a custom class inheriting from ObservableCollection and INotifyPropertyChanged (i.e. the custom class also has properties) that serves as a Collection<T> where T also inherits from INotifyPropertyChanged:
public class CustomCollection<T> : ObservableCollection<T>, INotifyPropertyChanged where T: INotifyPropertyChanged {
private string _name;
public string Name {
get {
return _name;
}
set {
if (_name != value) {
_name = value;
NotifyPropertyChanged("Name");
}
}
}
private int _total;
public int Total {
get {
return _total;
}
set {
if (_total != value) {
_total = value;
NotifyPropertyChanged("Total");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propertyName) {
PropertyChangedEventHandler handler = PropertyChanged;
if (null != handler) {
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
And T item class:
public class DataItem : INotifyPropertyChanged {
private string _fname;
public string Fname {
get {
return _fname;
}
set {
if (value != _fname) {
_fname = value;
NotifyPropertyChanged("Fname");
}
}
}
private int_value;
public int Value {
get {
return _value;
}
set {
if (value != _value) {
_value = value;
NotifyPropertyChanged("Value");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propertyName) {
PropertyChangedEventHandler handler = PropertyChanged;
if (null != handler) {
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
And the ViewModel:
public class ViewModel : ViewModelBase {
private readonly IService _dataService;
private bool _isLoading;
public bool IsLoading {
get {
return _isLoading;
}
private set {
_isLoading = value;
RaisePropertyChanged("IsLoading");
}
}
private CustomCollection<DataItem> _items;
public CustomCollection<DataItem> Items
{
get
{
return _items;
}
set
{
_items= value;
RaisePropertyChanged("Items");
}
}
public ViewModel(IService dataService) {
_dataService = dataService;
}
public void Refresh() {
if (!this.IsLoading) {
this.IsLoading = true;
_dataService.RefreshData(
this, (error) => {
if (error != null) {
return;
}
if (!IsInDesignMode)
this.IsLoading = false;
}
);
}
}
public void GetData() {
if (Games == null) {
Games = new CustomCollection<DataItem>();
} else {
Games.Clear();
}
if (!this.IsLoading) {
this.IsLoading = true;
_dataService.GetData(
this, (error) => {
if (error != null) {
return;
}
if (!IsInDesignMode)
this.IsLoading = false;
}
);
}
}
And I have bound the CustomCollection<T> to a control in my View (xaml). Everything works fine initially, upon navigating to the page, the ViewModel calls for a DataService to retrieve the data and populate the CustomCollection<T>. However, when refreshing the data, the View is not updated until all the data has been iterated over and refreshed/updated!
Here is the code for the refresh/updated (keep in mind, I'm retrieving the data via a web service, and for the purposes of testing have just manually updated the Value property in DataItem at each passover of the CustomCollection<T>):
public async RefreshData(ViewModel model, Action<Exception> callback) {
if (model.Items == null) return;
// ... retrieve data from web service here (omitted) ...
foreach (DataItem item in retrievedItems) { // loop for each item in retrieved items
DataItem newItem = new DataItem() { Fname = item.Fname, Value = item.Value };
if (model.Items.contains(newItem)) { // override for .Equals in CustomCollection<T> allows for comparison by just Fname property
model.Items[model.Items.IndexOf(newItem)].Value += 10; // manual update
} else {
model.Items.Add(newItem);
}
System.Threading.Thread.Sleep(1000); // 1 second pause to "see" each item updated sequentially...
}
callback(null);
}
So in summary, how can I make it so updating Value of my DataItem will instantly reflect in the View, given my current setup of CustomCollection<DateItem>? Something to do with async perhaps? I mean, when Sleep(1000) gets called, the UI does not hang, maybe this has something to do with it?
Any ideas on how to fix this? As you might have guessed, this issue is also present when first retrieving the data (but is barely noticeable as data is retrieved/processed during the navigation to the View).
Note: I'm using the MVVMLight Toolkit.
Thanks.
In XAML, i have a textblock
<TextBlock x:Name="block" Text="{Binding b1}"/>
and in c# i created a property
public int _b1;
public int b1
{
get { return _b1; }
set
{
_b1 = value;
}
}
public MainPage()
{
InitializeComponent();
block.DataContext = this;
}
this worked fine, textblock show the _b1. But when i add a button to chage the _b1 variable
private void bt_click(object sender, RoutedEventArgs e)
{
_b1 = 4;
}
the textblock didn't update ?????
To add to dotNet's answer (which is the correct answer), use a baseclass where you implement INotifyPropertyChanged if you want to avoid redundand code: (this is one example, there are other ways to implement this)
public abstract class BindableBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (Equals(storage, value)) { return false; }
storage = value;
OnPropertyChanged(propertyName);
return true;
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var eventHandler = PropertyChanged;
if (eventHandler != null)
{
eventHandler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
And use it like so:
class MyClass: BindableBase
{
private int _b1;
public int B1
{
get { return _b1; }
set { SetProperty(ref _b1, value); }
}
}
For UI to update automatically upon property value change, your property needs to either be a DependencyProperty or your class needs to implement INotifyPropertyChanged interface.
For creating a DependencyProperty, you could use Visual Studio's propdp snippet (type propdp inside your class and press Tab) and fill in respective values. If you want to go INotifyPropertyChanged path, you'll need to write the following code in the setter of your property (AFTER setting the value of _b1):
if(PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("b1"));
something i do wrong? anyone gives some suggestions
according to msdn
Indexers of a property can be specified within square brackets following the property name where the indexer is applied. For instance, the clause Path=ShoppingCart[0] sets the binding to the index that corresponds to how your property's internal indexing handles the literal string "0". Multiple indexers are also supported.
i put Indexers of a property in my xaml
<Image Source="{Binding ImagePathList[0]}" Height="50" Width="50" Grid.Row="0" Grid.Column="0" VerticalAlignment="Top" Margin="0,7,7,0" Grid.RowSpan="2">
i do not give the viewmodel code because i am pretty sure ListImagePathList have data.
EDIT*
more detail: ImagePathList[0] is a web image url
EDIT FOR Brendan
model is Article
public class Article : INotifyPropertyChanged
{
private long _Id;
public long ID
{
get { return _Id; }
set
{
if (_Id != value)
{
_Id = value;
NotifyPropertyChanged();
}
}
}
private string _subject;
public string Subject
{
get
{
return _subject;
}
set
{
if (_subject != value)
{
_subject = value;
NotifyPropertyChanged();
}
}
}
private string _words;
public string Words
{
get
{
return _words;
}
set
{
if (_words != value)
{
_words = value;
NotifyPropertyChanged();
}
}
}
private DateTime _publishDate;
public DateTime PublishDate
{
get
{ return _publishDate; }
set
{
if (_publishDate != value)
{
_publishDate = value;
NotifyPropertyChanged();
}
}
}
public List<string> ImagePathList = new List<string>();
private string _firstImage;
public string FirstImage
{
get
{
return _firstImage;
}
set
{
if (_firstImage != value)
{
_firstImage = value;
NotifyPropertyChanged();
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChangedEventHandler handler = PropertyChanged;
if (null != handler)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
ArticleViewModel is in below; All data returned from network is correct!
public class ArticleListViewModel : INotifyPropertyChanged
{
public ArticleListViewModel()
{
this.ArticleCollection = new ObservableCollection<Article>();
}
public ObservableCollection<Article> ArticleCollection
{
get;
private set;
}
public void LoadPage(int pageNumber)
{
if (pageNumber == 1)
{
this.ArticleCollection.Clear();
}
IsLoading = true;
ReadArticleList(pageNumber);
}
private async void ReadArticleList(int pageNumber)
{
try
{
List<Article> articleList = new List<Article>();
articleList = await CollectionHttpClient.GetArticlesByPageAsync(pageNumber);
this.ArticleCollection.Add(item);
}
}
catch(Exception ex)
{
if (ex.HResult == -2146233088 && ex.Message.Equals("Response status code does not indicate success: 404 ()."))
{
MessageBox.Show("The network is not set right. Internet cannot be accessed.");
}
else
{
MessageBox.Show("sorry, no data.");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (null != handler)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
The XAML code you show is fine.
There may be a problem with the DataContext. Maybe the page DataContext has not been set? Or maybe the the DataContext has changed e.g. inside an ItemTemplate
Otherise the problem is probably to do with the bound property. Try the following
private ObservableCollection<string> _imagePathList = new ObservableCollection<string>();
public ObservableCollection<string> ImagePathList {
get { return this._imagePathList; }
set {
if (this._imagePathList != value)
{
this._imagePathList = value;
// I'm going to assume you have the NotifyPropertyChanged
// method defined on the view-model
this.NotifyPropertyChanged();
}
}
}
ObservableCollection is in System.Collections.ObjectModel and is like List but if elements are added/removed then the PropertyChanged event is fired. Also note that any bound property must have a get associated with it for it to work at all.
Another possibility is that ImagePathList was not assigned to or is empty - in which case, make sure you assign to it!
In case you have not yet implemented the NotifyPropertyChanged method, here it is ...
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
You will also need to add INotifyPropertyChanged interface to the containing class e.g.
public class MyViewModelClass : INoftifyPropertyChanged
{
...
}
I think the problem concerns the place where your images come from. As it is said at MSDN:
You can set the Source by specifying an absolute URL (e.g. http://contoso.com/myPicture.jpg) or specify a URL relative to the XAP file of your application.
You can set this property in XAML, but in this case you are setting the property as a URI. The XAML behavior relies on underlying type conversion that processes the string as a URI, and calls the BitmapImage(Uri) constructor. This in turn potentially requests a stream from that URI and returns the image source object.
Just for test - place some images in your project and then set ImagePathList to them. See if that works (it should as I think). Or see if you can get BitmapImage(ImagePathList);
It's hard for me to say now (as I don't know how your ImagePathList looks like) what could be a reason of the failure. But if I were you, I would test this.
I would advise to use a property (or you can use converter which will also do the job):
// a property in your class
public Uri FirstImage
{
get
{
return new Uri(ImagePathList.FirstOrDefault(), UriKind.Absolute);
}
}
On the other hand if you are downloading images to IsolatedStorage then you will have to use another property, that will load BitmapImage from IS:
public BitmapImage FirstImage // getter - BitmapImage
{
get
{
if (String.IsNullOrEmpty(ImagePath[0])) return null;
BitmapImage temp = new BitmapImage();
using (IsolatedStorageFile ISF = IsolatedStorageFile.GetUserStoreForApplication())
using (IsolatedStorageFileStream file = ISF.OpenFile(ImagePath[0], FileMode.Open, FileAccess.Read))
temp.SetSource(file);
return temp;
}
}
Hope this helps.
I'm trying to bind some XAML code to a property in my ViewModel.
<Grid Visibility="{Binding HasMovies, Converter={StaticResources VisibilityConverter}}">
...
</Grid>
My ViewModel is setup like this:
private bool _hasMovies;
public bool HasMovies
{
get { return _hasMovies; }
set { _hasMovies = value; RaisePropertyChanged("HasMovies"); }
}
In the constructor of the ViewModel, I set the HasMovies link:
MovieListViewModel()
{
HasMovies = CP.Connection.HasMovies;
}
in CP:
public bool HasMovies
{
get { return MovieList != null && MovieList.Count > 0; }
}
private ObservableCollection<Movie> _movies;
public ObservableCollection<Movie> MovieList
{
get { return _movies; }
set
{
_movies = value;
RaisePropertyChanged("MovieList");
RaisePropertyChanged("HasMovies");
_movies.CollectionChanged += MovieListChanged;
}
}
private void MovieListChanged(object sender, NotifyCollectionChangedEventArgs e)
{
RaisePropertyChanged("HasMovies");
}
What am I doing wrong? How should I change this binding so that it reflects the current state of CP.Connection.HasMovies?
Either directly expose the object in the ViewModel and bind directly through that (so that the value is not just copied once which is what happens now) or subscribe to the PropertyChanged event and set HasMovies to the new value every time it changes in your source object.
e.g.
CP.Connection.PropertyChanged += (s,e) =>
{
if (e.PropertyName = "HasMovies") this.HasMovies = CP.Connection.HasMovies;
};
First of all, the setter for a collection type, such as your MovieList property, is not called when you change the content of the collection (ie. Add/Remove items).
This means all your setter code for the MovieList property is pointless.
Secondly, it's very silly code. A much better solution, is to use NotifyPropertyWeaver. Then your code would look like this, in the viewmodel:
[DependsOn("MovieList")]
public bool HasMovies
{
get { return MovieList != null && MovieList.Count > 0; }
}
public ObservableCollection<Movie> MovieList
{
get;
private set;
}
Alternatively you would have to add a listener for the CollectionChanged event when you initialize the MovieList property the first time (no reason to have a backing property, really really no reason!), and then call RaisePropertyChanged("HasMovies") in the event handler.
Example:
public class CP : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public CP()
{
MovieList = new ObservableCollection<Movie>();
MovieList.CollectionChanged += MovieListChanged;
}
public bool HasMovies
{
get { return MovieList != null && MovieList.Count > 0; }
}
public ObservableCollection<Movie> MovieList
{
get;
private set;
}
private void MovieListChanged(object sender, NotifyCollectionChangedEventArgs e)
{
RaisePropertyChanged("HasMovies");
}
private void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}