INotifyPropertyChanged implemented, DataContext set.
Here is my TreeView:
<TreeView ItemContainerStyle="{StaticResource MaterialDesignTreeViewItemExtended}" x:Name="TestCasesTree" MinWidth="220" ItemsSource="{Binding TestCases.Children, Mode=TwoWay}">
<i:Interaction.Behaviors>
<behaviours:BindableSelectedItemBehavior SelectedItem="{Binding SelectedTreeViewItem, Mode=TwoWay}" />
</i:Interaction.Behaviors>
<TreeView.Resources>
<con:TestAssessmentToImagePathConverter x:Key="TestAssessmentConverter" />
</TreeView.Resources>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children, Mode=TwoWay}" DataType="{x:Type data:TestCaseNode}">
<StackPanel Margin="0" Orientation="Horizontal">
<Image Source="{Binding TestAssessment, Converter={StaticResource TestAssessmentConverter}}" RenderOptions.BitmapScalingMode="HighQuality" Width="20" Height="20"/>
<TextBlock Margin="8 2 0 0" Text="{Binding Name}"/>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
Binding:
ItemsSource="{Binding TestCases.Children, Mode=TwoWay}"
BindableBase class:
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 bool CanExecute
{
get
{
return true;
}
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Property:
public TestCaseNode TestCases
{
get { return mainWindowModel.TestCases; }
set
{
mainWindowModel.TestCases = value;
SetProperty(ref _testCases, value);
}
}
Children:
public ObservableCollection<TestCaseNode> Children { get; set; }
The place where i edit something:
SelectedTreeViewItem.SetTestAssessmentForCluster(testAssessment);
OnPropertyChanged("Children");
OnPropertyChanged("TestCases");
OnPropertyChanged(nameof(TestCases));
OnPropertyChanged(nameof(TestCases.Children));
Nothing changes in View (but when i debug it in Visual Studio, i clearly see that the object changes)
This works, but it crashes another things:
SelectedTreeViewItem.SetTestAssessmentForCluster(testAssessment);
var tc = TestCases;
TestCases = null;
TestCases = tc;
UPD:
TestCaseNode Class:
public class TestCaseNode : TestCase, IEquatable<TestCaseNode>, IComparable<TestCaseNode>
{
public TestCaseNode Parent { get; set; }
public ObservableCollection<TestCaseNode> Children { get; set; }
public void Add(TestCaseNode node) {...}
public void SetTestAssessmentForCluster(TestAssessment testAssessment, ref TestScores _testScores) {...}
public bool Equals(TestCaseNode other) {...}
public override bool Equals(object obj) {...}
public override int GetHashCode() {...}
public int CompareTo(TestCaseNode other) {...}
}
TestCase Class:
public class TestCase
{
public string Id { get; set; } = string.Empty;
public string Name { get; set; }
public string Description { get; set; }
public ObservableCollection<TestStep> TestSteps { get; set; }
public TestAssessment TestAssessment { get; set; } = TestAssessment.EMPTY;
}
The OnPropertyChanged("TestCases"); must be issued by the object that includes the property that changed. In your case, your class TestCaseNode will need to call the OnPropertyChanged method in order for the Treeview to recognize the change to the collection because that object holds the collection. That is, it is not simply some any property named "TestCases" that has changed but the property in a specific object. The reason: you could have more than one properties with this name in different classes. And, if there are multiple copies of the object, they each have a property with the same name.
What I have done, and this has worked for me, is add a public method within the TestCaseNode called UpdateProperties() that then calls OnPropertyChanged("TestCases"); This ensures that the property update is issued by the correct object. Then I call this method when I need to have the control updated.
There are several other ways to do the same thing but I find this a simple and direct approach. You could, for example, include `OnPropertyChanged("TestCases"); in your add method. I have not done this because 1) the OnPropertyChanged call occurs too often and 2) without the OnPropertyChanged call I can defer the update to the control until after I've made several or all of my Add's.
Related
In my WPF app, I want to use an ObservableCollection to contain some Wave classes. Each Wave has an ObservableCollection to contain some Couple class. It looks like:
ObservableCollection<Wave> Waves { get; set; }
- int StartYear { get; set; }
- ObservableCollection<Couple> Couples { get; set; }
- int A { get; set; }
- int B { get; set; }
- some other Properties
After Waves have been added, int properties work well. However, the Couples in every Wave changes when each of them has been changed. They have the same GUID.
How can Coupless in every Wave are different?
Here is my code:
// Couple.cs
public class Couple : DependencyObject
{
public int A { get { return (int)GetValue(AProperty); } set { SetValue(AProperty, value); } }
public int B { get { return (int)GetValue(BProperty); } set { SetValue(BProperty, value); } }
public static readonly DependencyProperty AProperty = DependencyProperty.Register("A", typeof(int), typeof(Couple), new PropertyMetadata(0));
public static readonly DependencyProperty BProperty = DependencyProperty.Register("B", typeof(int), typeof(Couple), new PropertyMetadata(0));
}
// Wave.cs
class Wave : DependencyObject
{
public ObservableCollection<Couple> Couples
{
get { return (ObservableCollection<Couple>)GetValue(CouplesProperty); }
set { SetValue(CouplesProperty, value); }
}
public static readonly DependencyProperty CouplesProperty = DependencyProperty.Register("Couples", typeof(ObservableCollection<Couple>), typeof(Wave), new PropertyMetadata(new ObservableCollection<Couple>()));
}
// XAML of the UserControl
<ItemsControl Grid.Column="1" ItemsSource="{Binding Waves}" ScrollViewer.HorizontalScrollBarVisibility="Visible" Margin="0,0,0,6" Focusable="False" ScrollViewer.CanContentScroll="False" VirtualizingStackPanel.IsVirtualizing="True" VirtualizingStackPanel.VirtualizationMode="Recycling">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Width="240">
<ItemsControl ItemsSource="{Binding Couples}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40"/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="01" TextAlignment="Center"/>
<TextBox Grid.Column="1" Text="{Binding A}"/>
<TextBox Grid.Column="2" Text="{Binding B}"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
I am new to MVVM. Perviously, the values are got by use Items property of ListBox, but now I want to change these code to adapt to MVVM mode. Thank you!
The problem here is that you set a non-null default value for a dependency property that is of a mutable reference type. All instances of the class that owns the property (i.e. all Wave objects) will use the same default collection object.
The constructor of the Wave class should set an initial value like
class Wave
{
public static readonly DependencyProperty CouplesProperty =
DependencyProperty.Register(
nameof(Couples),
typeof(ObservableCollection<Couple>),
typeof(Wave));
public Wave()
{
Couples = new ObservableCollection<Couple>();
}
...
}
Besides that, you should not derive your view model classes from DependencyObject. They should instead implement the INotifyPropertyChanged interface if necessary:
public class Couple : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private int a;
public int A
{
get { return a; }
set
{
a = value;
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(nameof(A)));
}
}
private int b;
public int B
{
get { return b; }
set
{
b = value;
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(nameof(B)));
}
}
}
class Wave
{
public ObservableCollection<Couple> Couples { get; }
= new ObservableCollection<Couple>();
}
Note that the Couples property is now read-only, and the owning class does hence not need to fire a change notification for that property.
For the INotifyPropertyChanged implementation you would typically have a base class that encapsulates the invokation of the PropertyChanged event, like
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected void SetValue<T>(ref T field, T value, string propertyName)
{
if (!EqualityComparer<T>.Default.Equals(field, value))
{
field = value;
NotifyPropertyChanged(propertyName);
}
}
}
and use it like
public class Couple : ViewModelBase
{
private int a;
public int A
{
get { return a; }
set { SetValue(ref a, value, nameof(A)); }
}
private int b;
public int B
{
get { return b; }
set { SetValue(ref b, value, nameof(B)); }
}
}
I'm having an issue with my combo box. Somehow it can get out of sync with itself. For example, after I change out my BlockSequenceFields, only the dropdown text gets altered. Below, the Field 1 has been updated but you can see that it doesn't reflect in the currently selected item.
My IsSynchronizedWithCurrentItem=true should make the currently selected item behave as expected but it doesn't seem to work. I've read many stackoverflow posts where the current item doesn't match but they just set IsSynchronizedWithCurrentItem to true and it fixes their issue.
Can anyone explain why this isn't working for me?
<ComboBox x:Name="SequenceFieldComboBox"
SelectedItem="{Binding BlockSequenceFieldIndex, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ItemsSource="{Binding BlockSequenceFields, UpdateSourceTrigger=PropertyChanged}"
IsSynchronizedWithCurrentItem="True">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox
IsChecked="{Binding IsCalibrated, Mode=OneWay}"
IsEnabled="False">
</CheckBox>
<TextBlock
Text="{Binding}">
</TextBlock>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
EDIT: Further details for Mr. Chamberlain
// ViewModelBase implements INotifyPropertyChanged
public class BlockFieldViewModel : ViewModelBase
{
public BlockSequenceField SequenceField { get; set; }
public List<BlockSequenceCalibrationItemViewModel> Calibrations => this.SequenceField?.CalibrationList;
public bool IsCalibrated => this.Calibrations.TrueForAll(x => x.IsCalibrated == null || x.IsCalibrated == true);
public double AmplitudeThreshold => this.Calibrations.Max(x => x.Amplitude);
public int FieldNumber { get; set; }
public override string ToString()
{
string ret = string.Format(CultureInfo.CurrentCulture, "Field {0} ", this.FieldNumber);
if (Math.Abs(this.AmplitudeThreshold) > .00001)
{
ret = string.Concat(ret, string.Format(CultureInfo.CurrentCulture, "({0} mA)", this.AmplitudeThreshold));
}
return ret;
}
}
And here is the larger viewmodel, call it MainViewModel.cs. Here are the relevant fields in the class
private ObservableCollection<BlockFieldViewModel> blockSequenceFields;
public ObservableCollection<BlockFieldViewModel> BlockSequenceFields
{
get => this.blockSequenceFields;
set
{
this.blockSequenceFields = value;
this.OnPropertyChanged("BlockSequenceFields");
}
}
private void RefreshFieldList()
{
// In order for the combo box text to update, we need to reload the items
var savedIndex = this.BlockSequenceFieldIndex; // to restore to current field.
var fieldList = this.CalibrationViewModel.FieldViewModels;
this.BlockSequenceFields = new ObservableCollection<BlockFieldViewModel>(fieldList);
this.BlockSequenceFieldIndex = savedIndex;
}
Your problem is caused because BlockFieldViewModel does not raise INPC when FieldNumber is updated. You need to raise it for that property at the minimum.
//Assuming the base class looks like
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class BlockFieldViewModel : ViewModelBase
{
//...
public int FieldNumber
{
get
{
return _fieldNumber;
}
set
{
if(_fieldNumber.Equals(value))
return;
OnPropertyChanged();
}
}
//...
}
I don't know for sure if this will solve your problem or not, due to the fact that you are using .ToString() to display the name. If you find the above does not fix it trigger a property changed for the entire object by passing a empty string in to your OnPropertyChanged method
public int FieldNumber
{
get
{
return _fieldNumber;
}
set
{
if(_fieldNumber.Equals(value))
return;
//Refresh all properties due to the .ToString() not updating.
OnPropertyChanged("");
}
}
Also, if List<BlockSequenceCalibrationItemViewModel> Calibrations can be added to or removed from, or .Amplitude could be changed you need to trigger a refresh of the name from that too.
I develop CRUD app for WindowsPhone 8.1. I can add data to ObservableCollection collection and this data is displayed on ListBox. I use MVVM pattern.
Full repository https://github.com/OlegZarevych/CRUD_WP81
View :
<ListBox x:Name="Storage" ItemsSource="{Binding Path=Models, Mode=TwoWay}" >
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Height="30" Width="450">
<TextBlock x:Name="nameblock" Text="{Binding Name}" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
And ViewModel class
public class ViewModel
{
public string NewName { get; set; }
public string NewSurname { get; set; }
public int NewAge { get; set; }
public int i=0 ;
public ObservableCollection<DataStorageModel> Models { get; set; }
//Event Handlers
public ICommand CreateClickCommand { get; set; }
public ICommand UpdateClickCommand { get; set; }
public ICommand DeleteClickCommand { get; set; }
public ViewModel()
{
CreateClickCommand = new RelayCommand(arg => CreateClickMethod());
UpdateClickCommand = new RelayCommand(arg => UpdateClickMethod());
DeleteClickCommand = new RelayCommand(arg => DeleteClickMethod());
Models = new ObservableCollection<DataStorageModel>() {};
}
private void CreateClickMethod()
{
Models.Add(new DataStorageModel() { Name = NewName, Surname = NewSurname, Age = NewAge, Count=i++ });
}
private void UpdateClickMethod()
{}
private void DeleteClickMethod()
{}
}
I want to change data and delete it. As i good understand, I need select count from ListBoxItems and delete(update) this count in ObservableCollection.
How can I work with XAML code from ViewModel class ?
How can I initiliaze Storage in ViewModel ?
Or in MVVM is the better way to resolve this problem ?
When you want to delete a model from the ListBox you typically need some way to identify the selected ListBoxItems (or models) that you want to delete; for that, consider having an IsSelected property on your models and bind it to a CheckBox inside the ListBoxItem data template.
Now, when you click on delete, the delete command can then easily look into the Models list and see which items are selected for deletion. After it deletes the items, it can then enumerate over the collection and recalculate the count value for the remaining items and update the field in the view model.
So, you don't have to access the XAML to update the count of the models. If you make the count property mutable then you wouldn't have to reinitialize the storage after you delete items from the list.
I added code t the Model
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
if (_isSelected != value)
{
_isSelected = value;
OnPropertyChanged("IsSelected");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Also added checkbox with bindin to View.
<ListBox x:Name="Storage" Background="Gray" FontSize="14" ItemsSource="{Binding Path=Models, Mode=TwoWay}" >
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Height="60" Width="400" >
<CheckBox x:Name="checkbox" IsChecked="{Binding Path=IsSelected, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock x:Name="nameblock" Text="{Binding Name}"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
But IsSelected var doesn't change when I check checkbox in item
Why ?
I am working on my application update and I want to use a new searchbox and I want to show my results like Windows Store .
how can I do this ?
You can use an AutoSuggestBox which is bound to a changing ObservableCollection everytime the Text inside the AutoSuggestBox is changed.
For example, this is your Model:
public class App
{
public ind Id { get; set; }
public string Name { get; set; }
public string Category { get; set; }
public Image Picture { get; set; }
}
You can implement a method updating an ObservableCollection with a parameter (in this case the search expression) in your ViewModel:
public class ViewModel : INotifyPropertyChanged
{
public ViewModel()
{
SuggestedApps = new ObservableCollection<App>();
SuggestedApps.CollectionChanged += SuggestedApps_CollectionChanged;
}
private void SuggestedApps_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
OnPropertyChanged("SuggestedApps");
}
private ObservableCollection<App> suggestedApps;
public ObservableCollection<App> SuggestedApps
{
get
{
return suggestedApps;
}
set
{
suggestedApps = value;
OnPropertyChanged("SuggestedApps");
}
}
public void SuggestForSearch(string searchExpression)
{
SuggestedApps.Clear();
//Assumgin EF as DataSource
//You can use another Search algorithm here instead of String.Contains
foreach(var item in yourDataSource.Apps.Where(x => x.Name.Contains(searchExpression.Trim())))
{
SuggestedApps.Add(item);
}
}
public void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
In your Xaml code you can use this to bind an AutoSuggestBox to it and define a Template:
<AutoSuggestBox x:Name="AutoSuggestBoxApps" ItemsSource="{Binding SuggestedApps}" TextChanged="AutoSuggestBoxApps_TextChanged">
<AutoSuggestBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Image Source="{Binding Picture}"/>
<StackPanel>
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Category}"/>
</StackPanel>
</StackPanel>
</DataTemplate>
</AutoSuggestBox.ItemTemplate>
</AutoSuggestBox>
In the implemetation of the TextChanged-Event you just call the SuggestForSearch Method from your ViewModel:
private void AutoSuggestBoxApps_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
{
(this.DataContext as ViewModel).SuggestForSearch((sender as AutoSuggestBox).Text);
}
There is a control for UWP named AutoSuggestBox that you should read up on:
https://msdn.microsoft.com/nb-no/library/windows/apps/xaml/windows.ui.xaml.controls.autosuggestbox.aspx
This should give you the tools you need to give the wanted functionality
In my ViewModel i Have base Card class and Deck class which contain Observable Collection of Cards. Here is how it is bound in XAML
<GridView ItemsSource="{Binding DeckCollection}" IsItemClickEnabled="True" Grid.Row="0">
<GridView.ItemTemplate>
<DataTemplate>
<Button Command="{Binding Path=??}"
CommandParameter=??
<Button.Content>
<Grid>
<Image
Source="{Binding ImagePath}"
Stretch="None"/>
</Grid>
</Button.Content>
</Button>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
Here are my classes
class Deck
{
private ObservableCollection<Card> _deckCollection = new ObservableCollection<Card>();
public ObservableCollection<Card> DeckCollection
{
get { return _deckCollection; }
set { _deckCollection = value; }
}
public Deck()
{
ActionCommand = new MyCommand();
ActionCommand.CanExecuteFunc = obj => true;
ActionCommand.ExecuteFunc = AddToList;
}
public void AddToList(object parameter)
{
var clickedCard = this;
//add Card to list which in this case is not possible
//DeckCollection.Add(this) ?
}
}
class Card
{
public String Name { get; set; }
public int Cost { get; set; }
public String ImagePath { get; set; }
public MyCommand ActionCommand { get; set; }
}
And also MyCommand class
public class MyCommand : ICommand
{
public Predicate<object> CanExecuteFunc { get; set; }
public Action<object> ExecuteFunc { get; set; }
public bool CanExecute(object parameter)
{
return CanExecuteFunc(parameter);
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
ExecuteFunc(parameter);
}
}
I have made suggested changes but right now ActionCommand is not visible within collection, as only properties that belong to Card class can be bound.
EDIT:I have changed my XAML file for following but got some errors
<Button Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Deck}, Path=ActionCommand}}">
The property 'AncestorType' was not found in type 'RelativeSource'.
The property 'Path' was not found in type 'RelativeSource'.
The member "AncestorType" is not recognized or is not accessible.
The member "Path" is not recognized or is not accessible.
Unknown member 'AncestorType' on element 'RelativeSource'
Unknown member 'Path' on element 'RelativeSource'
Please help
If you want to have button which adds new items to your collection, I think something like that can be the solution.
In XAML:
<GridView ItemsSource="{Binding DeckCollection}" IsItemClickEnabled="True" Grid.Row="0">
<GridView.ItemTemplate>
<DataTemplate>
<Button>
<Button.Content>
<Grid>
<Image Source="{Binding ImagePath}"
Stretch="None"/>
</Grid>
</Button.Content>
</Button>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
<!-- public property located in Deck class -->
<Button Command="{Binding AddItemCommand}" Content="Add Item"/>
In C#:
class Deck, INotifyPropertyChanged /*custom implementation depends on .NET version, in my case its .NET3.5*/
{
private ObservableCollection<Card> _deckCollection = new ObservableCollection<Card>();
public ObservableCollection<Card> DeckCollection
{
get { return _deckCollection; }
set { _deckCollection = value;
OnPropertyChanged(() => DeckCollection); }
}
// your Add command
public ICommand AddItemCommand { get { return new MyCommand(AddToList); } }
private void AddToList(object parameter)
{
DeckCollection.Add(new Card());
}
public Deck() { }
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged<T>(Expression<Func<T>> expression)
{
if (PropertyChanged == null) return;
var body = (MemberExpression)expression.Body;
if (body != null) PropertyChanged.Invoke(this, new PropertyChangedEventArgs(body.Member.Name));
}
}
The main thing in this situation is that you cannot have the add button inside the collection.