So I am learning WPF by making a yatzee game, and I'm getting somewhere. But now I can not understand why I my "current roll" counter won't update in the View. I made the Dices work, and the roll dices button gives my dices new values - and this is updated in the view. But my "Current roll" variable will not. This is what I've done so far.
// CurrentRoll.cs
public class CurrentRoll : INotifyPropertyChanged
{
public int _roll;
public int Roll
{
get { return _roll; }
set
{
if (value != _roll)
{
_roll = value;
OnPropertyChanged("Roll");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
Then this is my DiceModelView. When I hit my "Roll Dices" button I get new values on all my dices as long as they are left unchecked. I also increment the _currentRoll variable, and only allow new rolls as long as _currentRoll still is less than 3. This logic works, the variable works, but it's representation in the View does not. I've also changed its initial value to other values just to see that my binding works, and it does.
public class DiceModelView : INotifyPropertyChanged
{
Die _die;
public CurrentRoll _currentRoll;
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<Die> myDices { get; set; }
public ICommand RollCommand { get; set; }
public DiceModelView()
{
myDices = new ObservableCollection<Die>()
{
new Die { Id = 0, Roll = 0, Checked = false },
new Die { Id = 1, Roll = 0, Checked = false },
new Die { Id = 2, Roll = 0, Checked = false },
new Die { Id = 3, Roll = 0, Checked = false },
new Die { Id = 4, Roll = 0, Checked = false }
};
_currentRoll = new CurrentRoll();
_currentRoll._roll = 0;
RollCommand = new Command (executeMethod, canexecuteMethod);
}
public bool canexecuteMethod(object parameter)
{
return true;
}
private void executeMethod(object parameter)
{
var r = new Random();
if (_currentRoll._roll < 3)
{
foreach (Die d in myDices)
{
if (d.Checked == false)
{
d.Roll = r.Next(1, 7);
}
}
}
_currentRoll._roll++;
}
private void NotifyPropertyChanged(String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public Die die
{
get { return _die; }
set { _die = value; }
}
public CurrentRoll currentRoll
{
get { return _currentRoll; }
set { _currentRoll = value;
NotifyPropertyChanged("currentRoll");
}
}
Finally, this is the XAML code I use:
<Window.DataContext>
<local:DiceModelView/>
</Window.DataContext>
<StackPanel>
<TextBlock Text="{Binding currentRoll.Roll, UpdateSourceTrigger=PropertyChanged}"/>
<ListView x:Name="DiceView" ItemsSource="{Binding myDices}" Width="500" HorizontalContentAlignment="Center" >
<ListView.ItemTemplate>
<DataTemplate >
<CheckBox Content="{Binding Roll}" IsChecked="{Binding Checked}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Button Content="Roll the dices!" Command="{Binding RollCommand}"/>
</StackPanel>
</Window>
You need to update the Roll property, not the _role field.
_roll should be private anyway.
Just think about it: You are raising the property changed event in the property setter, so this only get's executed when you set the value of the property.
Related
I'm having troubles with my UI not updating when my properties change, even though I have INotifyPropertyChange applied. When i run the code the first time, it shows up correct and the UI is updated. While debbuging I can see the new values being set to the strings of the viewmodel and that the OnPropertChange event is fired, it just don't happen anything in the UI. The code below will be in order of events. As extra information, I use the same code to update the viewmodel both in the first and second time.
public partial class Transaktioner : Window
{
ViewModelCommon.ViewModel view = new ViewModelCommon.ViewModel();
private static List<ViewModelCommon.Items2> getAccountingRowsListEdited = new List<ViewModelCommon.Items2>();
{
DataContext = view;
InitializeComponent();
}
private async Task GetAccountinTransactionsAsync()
{
await Task.Run(() =>
{
getAccountingRowsList = client.GetAccountingTransactions(ftglist[index], 0, ref status).ToList();
foreach (var v in getAccountingRowsList)
{
getAccountingRowsListEdited.Add(new ViewModelCommon.Items2
{
itemName2 = v.ver.ToString(),
value2 = v.text,
vertyp = v.vtyp,
s2 = v.kto.ToString(),
s3 = v.trdat.ToString()
});
}
Task.Run(async () =>
{
await SearchAndDisplayResult();
});
});
}
private async Task SearchAndDisplayResult(int exclusion = 0)
{
await Task.Run(() =>
{
var verfikationer = getAccountingRowsListEdited.Where(u => u.vertyp != exclusion).Count(u => u.s2.ToString().Equals("0"));
view.VerifikationerTotal = verfikationer.ToString();
});
}
The ViewModel:
class ViewModelCommon
{
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected bool SetProperty<T>(ref T field, T newValue, [CallerMemberName]string propertyName = null)
{
if (!EqualityComparer<T>.Default.Equals(field, newValue))
{
field = newValue;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
return true;
}
return false;
}
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
protected void Test(string sb)
{
Transaktioner tr = new Transaktioner("");
tr.ExcludeStringChanged(sb);
}
}
public class ViewModel : ViewModelBase
{
private string _verifikationerTotal;
public string VerifikationerTotal
{
get { return _verifikationerTotal; }
set
{
if (value != _verifikationerTotal)
{
_verifikationerTotal = value;
OnPropertyChanged("VerifikationerTotal");
}
}
}
private string _ExcludeString;
public string ExcludeString
{
get { return _ExcludeString; }
set
{
if (value != _ExcludeString)
{
_ExcludeString = value;
OnPropertyChanged("ExcludeString");
Test(ExcludeString);
}
}
}
}
The WPF:
<TextBox x:Name="TextBoxVerifikationerTotal" Text="{Binding VerifikationerTotal}" IsEnabled="False" HorizontalAlignment="Left" Height="23" Margin="583,182,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="99"/>
<TextBox HorizontalAlignment="Left" Height="23" Margin="837,10,0,0" TextWrapping="Wrap" Text="{Binding Path=ExcludeString, Mode=TwoWay, UpdateSourceTrigger=LostFocus}" VerticalAlignment="Top" Width="286"/>
The code above works as expected.
In the UI there is an option to introduce the optional attribute to exclude values. Those are bound to the "ExludeString" this also works and fires the event passing it again to the SearchAndDisplayResult(int exclusion = 0) with the replaces value of the int being passed. While debugging I can see that the event can successfully find a new value and passing it to the ViewModel, but it doesn't update the UI.
Are there any thoughts on why the UI is not updated? Thank you in advance!
The code has been shortend to show the vitals
Answer for this case was the
ViewModelCommon.ViewModel view = new ViewModelCommon.ViewModel();
not being set to a private static while working with Tasks.
I'm new to WPF and I'm having some trouble with my existing setup to get the list box selected item to appear in the text box.
The picture here represents the issue. I typed "12 HOUR" in the text box, which then filters the listbox to those items with "12 HOUR" anywhere in the string. But when I click "12 Hour Nasal" in the list box, I now want to reflect that choice back in the text box:
http://i.imgur.com/ZCYAolT.png
Here is my XAML for the user control containing the listbox and textbox:
<UserControl x:Class="SCM_AllergyRecModule.SearchAndSelectView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d">
<StackPanel Width="300">
<TextBox x:Name="Filter" Text="{Binding Path=Filter, UpdateSourceTrigger=PropertyChanged}"/>
<ListBox Width ="300" Height="50" x:Name="ListBoxControl"
ItemsSource="{Binding Path=Allergens}"
SelectedItem="{Binding Path=SelectedAllergen}">
</ListBox>
</StackPanel>
And here is the ViewModel:
namespace SCM_AllergyRecModule
{
public class SearchAndSelectViewModel
{
private ICollectionView allergens;
private string selectedAllergen;
private string filter = "";
public string Filter
{
get
{
return this.filter.ToUpperInvariant();
}
set
{
if (this.filter != value)
{
this.filter = value;
this.Allergens.Refresh();
}
}
}
private bool ContainsFilter(object item)
{
var product = item as string;
if (product == null)
{
return false;
}
if (string.IsNullOrEmpty(this.Filter))
{
return true;
}
if (product.ToUpperInvariant().Contains(this.Filter))
{
return true;
}
return false;
}
public SearchAndSelectViewModel()
{
var cvs = new CollectionViewSource();
cvs.Source = MainWindow.scmAllergens;
this.allergens = cvs.View;
this.allergens.Filter = ContainsFilter;
}
public ICollectionView Allergens
{
get
{
return this.allergens;
}
}
public string SelectedAllergen
{
get
{
return this.selectedAllergen;
}
set
{
if (this.selectedAllergen != value)
{
this.selectedAllergen = value;
}
}
}
}
}
Update 1
I added the INotifyPropertyChanged interface to my class and have it being raised on SelectedAllergen in the setter. I added an event handler called SearchAndSelectViewModel_PropertyChanged to handle the SelectedAllergen property changing and set it in the constructor.
Now when I click an item in the listbox, I do see it setting the Filter to the SelectedItem and the list filters to that item so nothing else shows. But still, the text box text is not changing? See screenshot below. This is after I typed in "PEAN" in the textbox, then the listbox filtered to two choices, and I chose "PEANUTS (FOOD)", which then refiltered the list box to just show that choice but didn't set the text box to "PEANUTS (FOOD)":
http://imgur.com/dNxuVI5
Updated ViewModel
public class SearchAndSelectViewModel : INotifyPropertyChanged
{
private ICollectionView allergens;
private string selectedAllergen;
private string filter;
public string Filter
{
get
{
return this.filter.ToUpperInvariant();
}
set
{
if (this.filter != value)
{
this.filter = value;
this.Allergens.Refresh();
}
}
}
private bool ContainsFilter(object item)
{
var product = item as string;
if (product == null)
{
return false;
}
if (string.IsNullOrEmpty(this.Filter))
{
return true;
}
if (product.ToUpperInvariant().Contains(this.Filter))
{
return true;
}
return false;
}
private void SearchAndSelectViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case "SelectedAllergen":
this.Filter = this.SelectedAllergen;
break;
}
}
public SearchAndSelectViewModel()
{
filter = "";
var cvs = new CollectionViewSource();
cvs.Source = MainWindow.scmAllergens;
this.allergens = cvs.View;
this.allergens.Filter = ContainsFilter;
this.PropertyChanged += SearchAndSelectViewModel_PropertyChanged;
}
public ICollectionView Allergens
{
get
{
return this.allergens;
}
}
public string SelectedAllergen
{
get
{
return this.selectedAllergen;
}
set
{
if (this.selectedAllergen != value && value != null)
{
this.selectedAllergen = value;
OnPropertyChanged("SelectedAllergen");
}
}
}
// INotifyPropertyChanged implementation
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
You need to implement the INotifyPropertyChanged interface and you can raise it in your property setter. Since you are also binding your TextBox to the Filter property you need to set the Filter property when your SelectedAllergen changes.
INotifyPropertyChanged example:
public class MyViewModel : INotifyPropertyChanged
{
//...
private int myProperty = 0;
public int MyProperty
{
get { return myProperty; }
set
{
myProperty = value;
// Raise the property changed notification
OnPropertyChanged("MyProperty");
}
}
// INotifyPropertyChanged implementation
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if(handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
there is probably a really simple reason why this isnt working but I've tried everything. I have a TextBlock with Text bound to a variable, the variable changes but the Text doesn't :
<TextBlock x:Name="modeLabel" Style="{StaticResource IndiTextBlock}" Height="23" TextWrapping="Wrap" Grid.Row="0" Text="{Binding ModeLabelText}" Margin="35,22,58,0"/>
The code that controls the text value is in a viewmodel:
public string ModeLabelText { get { return _modeLabeltext; } }
public ComboBoxItem SelectedMode { get { return _selectedMode; }
set
{
if (_selectedMode == value) return;
_selectedMode = value;
ToggleMode(null);
EvaluateScenario(null);
}
and
private void ToggleMode(object parameter)
{
if (_isBasicCalculation)
{
_modeLabeltext = "Target profit";
_isBasicCalculation = false;
}
else
{
_modeLabeltext = "Total to invest";
_isBasicCalculation = true;
}
}
Your class has to implement the INotifyPropertyChanged interface, and on changes of your variables, you should trigger the event
public class Model : INotifyPropertyChanged
{
public event EventHandler PropertyChanged; // event from INotifyPropertyChanged
protected void RaisePropertyChanged(string propertyName)
{
var local = PropertyChanged;
if (local != null)
{
local.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public void ToggleMode()
{
// ... your code ...
RaisePropertyChanged("ModelLabelText");
}
}
Thank you Nguyen Kien
private void ToggleMode(object parameter)
{
if (_isBasicCalculation)
{
_modeLabeltext = "Target profit";
OnPropertyChanged("ModeLabelText");
_isBasicCalculation = false;
}
else
{
_modeLabeltext = "Total to invest";
OnPropertyChanged("ModeLabelText");
_isBasicCalculation = true;
}
}
If I had an Observable collection like so :
public ObservableCollection<SpecialPriceRow> SpecialPriceRows = new ObservableCollection<SpecialPriceRow>();
SpecialPriceRow class :
public class SpecialPriceRow : INotifyPropertyChanged
{
public enum ChangeStatus
{
Original,
Added,
ToDelete,
Edited
}
public ChangeStatus Status { get; set; }
public string PartNo { get; set; }
private decimal _price;
public decimal Price
{
get
{
return _price;
}
set
{
if (value != _price)
{
_price = value;
Status = ChangeStatus.Edited;
OnPropertyChanged("Status");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
Would it be possible for me to bind a Label in the XAML to the count of objects that are say ... Added? So I could get something like this :
Where green is the count of "Added" objects within the collection. How would I go about doing something like this?
I've written up a ViewModel which will perform the desired functionality you are looking for.
class VM : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<SpecialPriceRow> _SpecialPriceRows = new ObservableCollection<SpecialPriceRow>();
private int _Original = 0;
private int _Added = 0;
private int _ToDelete = 0;
private int _Edited = 0;
public VM()
{
PropertyChanged = new PropertyChangedEventHandler(VM_PropertyChanged);
//The following lines in the constructor just initialize the SpecialPriceRows.
//The important thing to take away from this is setting the PropertyChangedEventHandler to point to the UpdateStatuses() function.
for (int i = 0; i < 12; i++)
{
SpecialPriceRow s = new SpecialPriceRow();
s.PropertyChanged += new PropertyChangedEventHandler(SpecialPriceRow_PropertyChanged);
SpecialPriceRows.Add(s);
}
for (int j = 0; j < 12; j+=2)
SpecialPriceRows[j].Price = 20;
}
private void VM_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
}
private void SpecialPriceRow_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "Status")
UpdateStatuses();
}
public ObservableCollection<SpecialPriceRow> SpecialPriceRows
{
get
{
return _SpecialPriceRows;
}
}
private void UpdateStatuses()
{
int original = 0, added = 0, todelete = 0, edited = 0;
foreach (SpecialPriceRow SPR in SpecialPriceRows)
{
switch (SPR.Status)
{
case SpecialPriceRow.ChangeStatus.Original:
original++;
break;
case SpecialPriceRow.ChangeStatus.Added:
added++;
break;
case SpecialPriceRow.ChangeStatus.ToDelete:
todelete++;
break;
case SpecialPriceRow.ChangeStatus.Edited:
edited++;
break;
default:
break;
}
}
Original = original;
Added = added;
ToDelete = todelete;
Edited = edited;
}
public int Original
{
get
{
return _Original;
}
set
{
_Original = value;
PropertyChanged(this, new PropertyChangedEventArgs("Original"));
}
}
public int Added
{
get
{
return _Added;
}
set
{
_Added = value;
PropertyChanged(this, new PropertyChangedEventArgs("Added"));
}
}
public int ToDelete
{
get
{
return _ToDelete;
}
set
{
_ToDelete = value;
PropertyChanged(this, new PropertyChangedEventArgs("ToDelete"));
}
}
public int Edited
{
get
{
return _Edited;
}
set
{
_Edited = value;
PropertyChanged(this, new PropertyChangedEventArgs("Edited"));
}
}
}
Take note of the comments in the constructor. You need to point the PropertyChanged event of each SpecialPriceRow to the UpdateStatuses function in order for this to work properly.
Now all you need to do is bind your labels to the appropriate properties in the ViewModel.
If your SpecialPriceRows list becomes very large, you may want to consider calculating the status counts a bit differently. Currently, it is iterating through the entire list every time one instance is updated. For this to perform better, you may want to keep the old value of the status in the SpecialPriceRow class and every time an update occurs, increment the new status count and decrement the old one.
I'm not aware of any builtin functionality to do this. I would create a custom property in your data context class that does the counting and bind to this.
Something like this:
public int AddedCount
{
get
{
return SpecialPriceRows.Where(r => r.Status == ChangeStatus.Added).Count();
}
}
Then whenever an item is changed or added you need to explicitly raise the property changed for this:
public void AddItem()
{
...
OnPropertyChanged("AddedCount");
}
Then you only need to bind in your XAML code like:
<TextBlock Text="{Binding AddedCount}" />
You may need to subscribe to the change events in your collection to know when an item changes.
Alternative:
You can also create a ListCollectionView with a specific filter and bind to its Count property:
AddedItems = new ListCollectionView(TestItems);
AddedItems.Filter = r => ((SpecialPriceRow)r).Status == ChangeStatus.Added;
In your XAML you would then bind to the Count property of this:
<TextBlock Text="{Binding AddedItems.Count}" />
This has the advantage that it will automatically keep track of added and removed items in the collection and update itself. You have to refresh it manually though when the property of an item changes which affects the filter.
I have a ComboBox, whose ItemsSource is bound to a new (not default) ListCollectionView, which is linked to an ObservableCollection. The ComboBox SelectedItem property is bound to a public SelectedHat property.
Step 1: Select the 2nd item in the ComboBox. SelectedHat is now the 2nd Hat in the list, as expected.
Step 2: (Click the button to) Set the 2nd spot in the list to a new Hat. SelectedHat is first set to null, then set to the new Hat.
Why is SelectedHat set to null before the new Hat?
I want to be able to vm.Collection[index] = new Hat() and
(1) if the ComboBox has that index selected, keep it selected instead of going blank
(2) only set SelectedHat once to the new Hat, instead of null and THEN the new Hat
C#:
public partial class MainWindow : Window
{
private readonly ViewModel vm;
public MainWindow()
{
InitializeComponent();
vm = new ViewModel();
DataContext = vm;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
Hat item = new Hat { Name = "hat 2", Color = "Red"};
vm.Collection[1] = item;
}
}
public class ViewModel : BaseNotifyPropertyChanged
{
public ObservableCollection<Hat> Collection { get; set; }
public ListCollectionView View { get; set; }
private Hat selectedHat;
public Hat SelectedHat
{
get { return selectedHat; }
set
{
selectedHat = value;
Console.WriteLine(string.Format("SelectedHat set to [{0}]", value));
NotifyPropertyChanged("SelectedHat");
}
}
public ViewModel()
{
Collection = new ObservableCollection<Hat>()
{
new Hat { Name = "hat 1", Color = "Black" },
new Hat { Name = "hat 2", Color = "Black" },
new Hat { Name = "hat 3", Color = "Black" },
};
View = new ListCollectionView(Collection);
View.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
}
}
public class Hat
{
public string Name { get; set; }
public string Color { get; set; }
public override string ToString()
{
return string.Format("{0} ({1})", Name, Color);
}
}
public abstract class BaseNotifyPropertyChanged : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void NotifyPropertyChanged(String propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
XAML:
<StackPanel>
<TextBlock Text="{Binding Path=SelectedHat, Mode=OneWay}" />
<ComboBox ItemsSource="{Binding Path=View}" SelectedItem="{Binding Path=SelectedHat, UpdateSourceTrigger=PropertyChanged}" />
<Button Content="click me" Click="Button_Click" />
</StackPanel>
This is the implementation of ObservableCollection.SetItem
protected override void SetItem(int index, T item)
{
this.CheckReentrancy();
T obj = this[index];
base.SetItem(index, item);
this.OnPropertyChanged("Item[]");
this.OnCollectionChanged(NotifyCollectionChangedAction.Replace, (object) obj, (object) item, index);
}
as you can see it raises OnPropertyChanged("Item[]") and then OnCollectionChanged(NotifyCollectionChangedAction.Replace, (object) obj, (object) item, index).
OnCollectionChanged has parameters 'oldItem' and 'newItem'. I expect if we traced the code through to the combo box implementation we would see that the old item was removed and replaced with null, and then the new item was inserted, which is why you get the behaviour your experiencing (I see it as well).
My work around is instead of replacing the item, add the new item, change the currently selected item and then remove the old item.
private void ButtonClick(object sender, System.Windows.RoutedEventArgs e)
{
Hat newHat = new Hat { Name = "hat 2", Color = "Red" };
var viewModel = (ViewModel)DataContext;
var oldHat = viewModel.Collection[1];
if (viewModel.SelectedHat == oldHat)
{
viewModel.Collection.Add(newHat);
viewModel.SelectedHat = newHat;
viewModel.Collection.Remove(oldHat);
}
else
{
viewModel.Collection[1] = newHat;
}
}
Change the Item directly. I just tested it. vm.Collection[1].name = "hat 2"