I bind a class which derived from INotifyPropertyChange to a Datacontext.
after some interaction, a value will be calculated and output property will be updated.
My problem is that the result textbox didn't update at all.
public partial class setraSubWpfTolerance : UserControl
{
public setraFit objSource = new setraFit();
public setraSubWpfTolerance()
{
InitializeComponent();
this.DataContext = objSource;
}
}
And the class:
public class setraFit : INotifyPropertyChanged
{
private readonly CollectionView _BoreSystems;
public CollectionView BoreSystems
{
get { return _BoreSystems; }
}
private decimal? _MaxBoreDimension;
public decimal? MaxBoreDimension
{
get { return _MaxBoreDimension; }
set
{
if (_MaxBoreDimension == value) return;
_MaxBoreDimension = value;
onPropertyChanged("MaxBoreDimension");
}
}
private string _BoreSystem;
public string BoreSystem
{
get { return _BoreSystem; }
set
{
if (_BoreSystem == value) return;
_BoreSystem = value;
calcBoreDimension();
onPropertyChanged("BoreSystem");
}
}
public setraFit()
{
IList<string> listBore = setraStaticTolerance.getBoreList();
_BoreSystems = new CollectionView(listBore);
}
public event PropertyChangedEventHandler PropertyChanged;
private void onPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
private void calcBoreDimension()
{
_MaxBoreDimension = (decimal)100.035;
}
}
Last but not least the XAML
<UserControl x:Class="SetraSubForms.setraSubWpfTolerance"
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"
d:DesignHeight="450" d:DesignWidth="375">
<Grid>
<ComboBox Height="23" HorizontalAlignment="Left" Margin="194,10,0,0" Name="BoreSystemComboBox" VerticalAlignment="Top" Width="120"
ItemsSource="{Binding Path=BoreSystems}"
SelectedValue="{Binding Path=BoreSystem}"/>
<TextBox HorizontalAlignment="Left" Margin="194,67,0,37" Name="MaxDimBoreTextBox" Width="120" IsReadOnly="False"
Text="{Binding Path=MaxBoreDimension, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>
</Grid>
</UserControl>
I expected to receive the dummy value of 100.035 after changing the combobox but the textbox did not update. If i run step by step i can see the "MaxBoreDimension" property of setraFit is changed.
What did i do wrong?
Thanks in advance for your help
sittingDuck
Your method is updating the private value, not the Property:
private void calcBoreDimension()
{
_MaxBoreDimension = (decimal)100.035;
}
Change to
private void calcBoreDimension()
{
MaxBoreDimension = (decimal)100.035;
}
You're doing the same thing in the constructor, which is causing your calcBoreDimension method to not run:
public setraFit()
{
IList<string> listBore = setraStaticTolerance.getBoreList();
_BoreSystems = new CollectionView(listBore);
}
should be
public setraFit()
{
IList<string> listBore = setraStaticTolerance.getBoreList();
BoreSystems = new CollectionView(listBore); //this line!
}
When you create properties that point to private fields, you should almost never have to set the private field anywhere other than the property. This is why properties exist- so that whenever you get or set them, you will run the code in the get and set blocks instead of just retrieving the current value.
SOLVED!
The key is to initate the PropertyChanged event for the "MaxBoreDimension"
public decimal? NominalDimension
{
get { return _NominalDimension; }
set
{
if (_NominalDimension == value) return;
_NominalDimension = value;
calcBoreDimension();
onPropertyChanged("NominalDimension");
onPropertyChanged("MaxBoreDimension");
}
}
Thanks DLeh for the contribution.
Related
I have this combobox:
<ComboBox Grid.Column="1" SelectedItem="{Binding SelectedItem}" ItemsSource="{Binding Items, Mode=OneWay}" HorizontalAlignment="Stretch" VerticalAlignment="Center"/>
and this is the code:
public class CustomComboBoxViewModel
{
private bool DiscardSelChanged { get; set; }
public ObservableCollection<string> Items { get; set; }
public string SelectedItem
{
get { return _selectedItem; }
set
{
if (!DiscardSelChanged)
_selectedItem = value;
bool old = DiscardSelChanged;
DiscardSelChanged = false;
if (!old)
SelectionChanged?.Invoke(_selectedItem);
}
}
public event Action<string> SelectionChanged;
public void AddItem(string item)
{
var v = Items.Where(x => x.Equals(item)).FirstOrDefault();
if (v != default(string))
{
SelectedItem = v;
}
else
{
DiscardSelChanged = true;
_selectedItem = item;
Items.Insert(0, item);
}
}
}
At startup I have only one item: Browse.... selecting it i can browse for a file and add its path to the ComboBox. AddItem method is called
If the selected file path doesn't exists in Items i add and select it (this is working).
If the selected file path exists in Items i want to automatically select it without adding it to the list again. this doesn't work and Browse... is the visualized item.
I already tried to use INotifyPropertyChanged.
I'm using .NET 4.6.2. Any ideas to get it working?
EDIT 4:barebone example
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Windows;
namespace WpfApp2
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window, INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
Items = new ObservableCollection<string>();
Items.Add(ASD);
}
private string ASD = #"BROWSE";
private string _selectedItem;
public string SelectedItem
{
get { return _selectedItem; }
set
{
_selectedItem = value;
OnPropertyChanged(nameof(SelectedItem));
UploadFileSelection_SelectionChanged();
}
}
public ObservableCollection<string> Items { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
private void AddItem(string item)
{
var v = Items.Where(x => x.Equals(item)).FirstOrDefault();
if (v != default(string))
SelectedItem = v;
else
{
Items.Add(item);
SelectedItem = item;
}
}
private void UploadFileSelection_SelectionChanged()
{
if (SelectedItem == ASD)
{
Microsoft.Win32.OpenFileDialog dlg = new Microsoft.Win32.OpenFileDialog()
{
DefaultExt = ".*",
Filter = "* Files (*.*)|*.*"
};
bool? result = dlg.ShowDialog();
if (result == true)
AddItem(dlg.FileName);
}
}
}
}
comboBox:
<ComboBox SelectedItem="{Binding SelectedItem}" ItemsSource="{Binding Items}"/>
try to:
- select FILE_A.txt
- select FILE_B.txt
- select FILE_A.txt again
I tried your example. I fixed the re-entrancy problem (double browse dialog) with a flag:
private bool _browsing = false;
private void UploadFileSelection_SelectionChanged()
{
if (_browsing)
{
return;
}
if (SelectedItem == ASD)
{
try
{
_browsing = true;
Microsoft.Win32.OpenFileDialog dlg = new Microsoft.Win32.OpenFileDialog()
{
DefaultExt = ".*",
Filter = "* Files (*.*)|*.*"
};
bool? result = dlg.ShowDialog();
if (result == true)
AddItem(dlg.FileName);
}
finally
{
_browsing = false;
}
}
}
It's caveman stuff but it works.
The real problem you have is that UploadFileSelection_SelectionChanged() is called, and updates SelectedItem before you exit the SelectedItem setter from the call that sets it to ASD.
So SelectedItem = v; in AddItem() has no effect on the combobox, because the combobox isn't responding to PropertyChanged right then.
This will fix that:
private void AddItem(string item)
{
var v = Items.FirstOrDefault(x => x.Equals(item));
if (v != default(string))
{
//SelectedItem = v;
Task.Run(() => SelectedItem = v);
}
else
{
Items.Add(item);
SelectedItem = item;
}
}
Now we're doing it later.
But note that the other branch does work, the one where item is newly added to the collection. You can also fake it out by removing item and adding it again:
private void AddItem(string item)
{
// Harmless, if it's not actually there.
Items.Remove(item);
Items.Add(item);
SelectedItem = item;
}
That looks weirder, but since it doesn't rely on thread timing, it's probably a better solution. On the other hand, this is "viewmodel" code whose details are driven by the peculiarities of the implementation of the ComboBox control. That's not a good idea.
This should be probably be done in the view (leaving aside that in this contrived example our view is our viewmodel).
You are setting _selectedItem without calling OnPropertyChanged() afterwards. That's why it is not working. If you want a clear code solution consider implementing the property with OnPropertyChanged() like this:
int _example;
public int Example
{
get
{
return _example;
}
set
{
_example = value;
OnPropertyChanged(nameof(Example);
}
}
Your code will be less error prone.
Do it as easy as possible:
public class ViewModel : INotifyPropertyChanged
{
public ObservableCollection<string> Strings { get; set; }
public ICommand AddAnotherStringCommand { get; set; }
string _selectedItem;
public string SelectedItem
{
get
{
return _selectedItem;
}
set
{
_selectedItem = value;
OnPropertyChanged(nameof(this.SelectedItem));
}
}
public int counter { get; set; } = 1;
public ViewModel()
{
// RelayCommand from: https://stackoverflow.com/questions/22285866/why-relaycommand
this.AddAnotherStringCommand = new RelayCommand<object>(AddAnotherString);
this.Strings = new ObservableCollection<string>();
this.Strings.Add("First item");
}
private void AddAnotherString(object notUsed = null)
{
this.Strings.Add(counter.ToString());
counter++;
this.SelectedItem = counter.ToString();
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Main Window:
<Window x:Class="Test.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Test"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:ViewModel x:Name="ViewModel" />
</Window.DataContext>
<StackPanel>
<ComboBox ItemsSource="{Binding Strings}" SelectedItem="{Binding SelectedItem}"/>
<Button Content="Add another item" Command="{Binding AddAnotherStringCommand}" />
</StackPanel>
</Window>
In my case the value is changed every time, but you should be able to modify the code to fit your needs.
Make sure that you have a clear code structure and do not overcomplicate things.
If you want a more specific answer you should consider to present you whole code.
Question
How can I make it so that changes to a note are only propagated back to the list, when the Save button is clicked instead on "lost focus"?
And the Save button should only be enabled when the note has been changed.
UI
The example application looks like this:
The current behaviour is:
Clicking on a note puts its text into the TextBox; that's fine.
The changed text from the TextBox gets written back to the list when the TextBox loses the focus (default binding behaviour); but I only want that to happend when the Save button is clicked.
The Save button is always activated because the CanExecute(object parameter) isn't correctly implemented yet; it should only get activated when the TextBox text is different from the selected note's text.
My research so far
Option 1: Some Internet sources say to bind a different property to the TextBox and to programmatically check whether it is different from the SelectedItem of the ListView. I would have hoped that there was a way without introducing a third property in addition to the already existing ListOfNotes and SelectedNote.
Option 2: Some Internet sources recommend to configure Mode=OneWay so that clicking an item in the ListView updates the TextBox, but not the other way around. This sounds like the solution I would prefer, but I wasn't able to figure out from the code examples how to raise an event programmatically so that the change in the TextBox gets written back to the ListView when the Save button is clicked.
I've found other Stackoverflow questions that seem to be similar to mine, but the answers to those haven't helped me fix the problem:
WPF databinding after Save button click
Code
This example currently does two-way binding on focus lost. How do I need to change it to get the above described behaviour?
https://github.com/lernkurve/WpfBindingOneWayWithSaveButton
MainWindow.xaml
<Window x:Class="WpfBindingOneWayWithSaveButton.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:wpfBindingOneWayWithSaveButton="clr-namespace:WpfBindingOneWayWithSaveButton"
mc:Ignorable="d"
Title="MainWindow" Height="188.636" Width="299.242">
<Window.DataContext>
<wpfBindingOneWayWithSaveButton:MainWindowsViewModel />
</Window.DataContext>
<Grid>
<GroupBox Header="List of notes" HorizontalAlignment="Left" VerticalAlignment="Top" Height="112" Width="129" Margin="0,24,0,0">
<ListView ItemsSource="{Binding ListOfNotes}" SelectedItem="{Binding SelectedNote}" DisplayMemberPath="Text" HorizontalAlignment="Left" Height="79" VerticalAlignment="Top" Width="119" Margin="0,10,-2,0"/>
</GroupBox>
<GroupBox Header="Change selected note" HorizontalAlignment="Left" Margin="134,24,0,0" VerticalAlignment="Top" Height="112" Width="151">
<Grid HorizontalAlignment="Left" Height="89" Margin="0,0,-2,0" VerticalAlignment="Top" Width="141">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40*"/>
<ColumnDefinition Width="101*"/>
</Grid.ColumnDefinitions>
<TextBox Text="{Binding SelectedNote.Text}" HorizontalAlignment="Left" Height="23" TextWrapping="Wrap" VerticalAlignment="Top" Width="121" Margin="10,7,0,0" Grid.ColumnSpan="2"/>
<Button Command="{Binding SaveCommand}" Content="Save" HorizontalAlignment="Left" VerticalAlignment="Top" Width="121" Margin="10,35,0,0" Grid.ColumnSpan="2"/>
</Grid>
</GroupBox>
</Grid>
</Window>
MainWindowsViewModel.cs
using System.Collections.ObjectModel;
using System.Windows.Input;
namespace WpfBindingOneWayWithSaveButton
{
public class MainWindowsViewModel
{
public ObservableCollection<Note> ListOfNotes { get; set; }
public Note SelectedNote { get; set; }
public ICommand SaveCommand { get; set; }
public MainWindowsViewModel()
{
ListOfNotes = new ObservableCollection<Note>
{
new Note { Text = "Note 1" },
new Note { Text = "Note 2" }
};
SaveCommand = new SaveCommand(this);
}
}
}
SaveCommand.cs
using System;
using System.Windows.Input;
namespace WpfBindingOneWayWithSaveButton
{
public class SaveCommand : ICommand
{
private MainWindowsViewModel vm;
public SaveCommand(MainWindowsViewModel vm)
{
this.vm = vm;
}
public bool CanExecute(object parameter)
{
// What should go here?
return true;
// Pseudo code
// return (is the TextBox text different from the original note text)
}
public void Execute(object parameter)
{
// What should go here?
// Pseudo code
// Let WPF know that the TextBox text has changed
// Invoke the binding so it propagates the TextBox text back to the list
}
public event EventHandler CanExecuteChanged;
}
}
Note.cs
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace WpfBindingOneWayWithSaveButton
{
public class Note : INotifyPropertyChanged
{
private string text;
public string Text
{
get { return text; }
set
{
text = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Bind the text to the CommandParameter of the SaveButton so it gets passed to the Save method for updating.
<TextBox x:Name="NoteTextBox" Text="{Binding SelectedNote.Text, Mode=OneTime}" ../>
<Button Command="{Binding SaveCommand}"
CommandParameter="{Binding ElementName=NoteTextBox, Path=Text}",
Content="Save" />
and
public bool CanExecute(object parameter)
{
return vm.SelectedNote.Text != parameter as string;
}
public void Execute(object parameter)
{
vm.SelectedNote.Text = parameter as string;
}
Option one is the easiest to implement, you will need to clone the Note object and set it to a separate property.
in your xaml, change your list view to the following so it now binds the SelectedIndex instead of the SelectedItem.
<ListView ItemsSource="{Binding ListOfNotes}" SelectedIndex="{Binding SelectedIndex}" DisplayMemberPath="Text" ...
And change TextBox to the following so it updates the binding as you type
<TextBox Text="{Binding Path=SelectedNote.Text, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" ...
In Note.cs we add the Clone() method.
public class Note : INotifyPropertyChanged
{
public Note Clone()
{
return new Note()
{
Text = this.Text
};
}
//... The rest stays the same
}
In MainWindowsViewModel.cs we add new properties for the SelectedIndex and clone the object when we detect a index has changed. We also need to add INotifyPropertyChanged so we can update the SelectedNote from the codebehind when we do the Clone()
public class MainWindowsViewModel : INotifyPropertyChanged
{
private int _selectedIndex = -1;
private Note _selectedNote;
public int SelectedIndex
{
get { return _selectedIndex; }
set
{
if (_selectedIndex.Equals(value))
return;
_selectedIndex = value;
CloneSelectedNote();
}
}
private void CloneSelectedNote()
{
if (SelectedIndex >= 0)
{
SelectedNote = ListOfNotes[SelectedIndex].Clone();
}
else
{
SelectedNote = null;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public Note SelectedNote
{
get { return _selectedNote; }
set
{
if(Equals(_selectedNote, value))
return;
_selectedNote = value;
OnPropertyChanged();
}
}
//... The rest stays the same
}
In SaveCommand.cs we add the logic for CanExecute and add the subscriptions to CommandManager.RequerySuggested, this automatically makes it requery the CanExecute any time any binding changes. This can be a little ineffecent, if you wanted to you could expose a RaiseCanExecuteChanged() publicly but it would be MainWindowsViewModel responsibility to call it any time vm.SelectedIndex or vm.SelectedNote.Text changed.
public class SaveCommand : ICommand
{
private MainWindowsViewModel vm;
public SaveCommand(MainWindowsViewModel vm)
{
this.vm = vm;
}
public bool CanExecute(object parameter)
{
if (vm.SelectedIndex < 0 || vm.SelectedNote == null)
return false;
return vm.ListOfNotes[vm.SelectedIndex].Text != vm.SelectedNote.Text;
}
public void Execute(object parameter)
{
vm.ListOfNotes[vm.SelectedIndex] = vm.SelectedNote;
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
}
UPDATE: Here is a updated version that does not use CommandManager
MainWindowsViewModel.cs
public class MainWindowsViewModel : INotifyPropertyChanged
{
private int _selectedIndex = -1;
private Note _selectedNote;
public int SelectedIndex
{
get { return _selectedIndex; }
set
{
if (_selectedIndex.Equals(value))
return;
_selectedIndex = value;
CloneSelectedNote();
RecheckSaveCommand();
}
}
private void CloneSelectedNote()
{
if (SelectedIndex >= 0)
{
SelectedNote = ListOfNotes[SelectedIndex].Clone();
}
else
{
SelectedNote = null;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public Note SelectedNote
{
get { return _selectedNote; }
set
{
if(Equals(_selectedNote, value))
return;
if (_selectedNote != null)
{
PropertyChangedEventManager.RemoveHandler(_selectedNote, SelectedNoteTextChanged, nameof(Note.Text));
}
_selectedNote = value;
if (_selectedNote != null)
{
PropertyChangedEventManager.AddHandler(_selectedNote, SelectedNoteTextChanged, nameof(Note.Text));
}
OnPropertyChanged();
}
}
private void SelectedNoteTextChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
{
RecheckSaveCommand();
}
private void RecheckSaveCommand()
{
var command = this.SaveCommand as WpfBindingOneWayWithSaveButton.SaveCommand; //"this." and "WpfBindingOneWayWithSaveButton." are not necessary but I wanted to be explicit.
if (command != null)
{
command.RaiseCanExecuteChanged();
}
}
//...
}
SaveCommand.cs
public class SaveCommand : ICommand
{
private MainWindowsViewModel vm;
public SaveCommand(MainWindowsViewModel vm)
{
this.vm = vm;
}
public bool CanExecute(object parameter)
{
if (vm.SelectedIndex < 0 || vm.SelectedNote == null)
return false;
return vm.ListOfNotes[vm.SelectedIndex].Text != vm.SelectedNote.Text;
}
public void Execute(object parameter)
{
vm.ListOfNotes[vm.SelectedIndex] = vm.SelectedNote;
}
public event EventHandler CanExecuteChanged;
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
You should not use OneWay but rather an UpdateSourceTrigger of value Explicit. BindingGroups can do this for you though, here's a simple example:
<!-- For change observation -->
<TextBlock Text="{Binding Text}"></TextBlock>
<StackPanel>
<StackPanel.BindingGroup>
<BindingGroup x:Name="EditGroup"></BindingGroup>
</StackPanel.BindingGroup>
<TextBox Text="{Binding Text}"></TextBox>
<Button>
<Button.Command>
<local:CommitGroupCommand BindingGroup="{x:Reference EditGroup}"/>
</Button.Command>
Save
</Button>
</StackPanel>
public class CommitGroupCommand : ICommand
{
public BindingGroup BindingGroup { get; set; }
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
BindingGroup.UpdateSources();
}
}
(You could add a validation rule to your binding that requires the value to be different and use that for the CanExecute implementation.)
Using this method allows you to bind directly to the object you intend to edit, so you don't need to copy around values first.
I'm trying to Bind the Text property of the TextBlock to StaticClass.Percent. Since I couldn't do that (Or could I?) I have defined the LoadingPercent in my ViewModel so that I can bind to it. It is still not working. How can I solve this? Or can I bind to the StaticClass directly and ignore the ViewModel approach?
<Window x:Class="TestBinding.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:testBinding="clr-namespace:TestBinding"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<testBinding:ViewModel/>
</Window.DataContext>
<StackPanel>
<TextBlock Width="100"
HorizontalAlignment="Center"
Text="{Binding LoadingPercent}"/>
<Button Content="Change"
Width="200"
Height="30"
Margin="0 20 0 0"
HorizontalAlignment="Center"
Click="ChangeText"/>
</StackPanel>
</Window>
public partial class MainWindow
{
public MainWindow()
{
InitializeComponent();
}
private void ChangeText(object sender, RoutedEventArgs e)
{
StaticClass.Percentage = 10;
}
}
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private double loadingPercent;
public double LoadingPercent
{
get { return StaticClass.Percentage; }
set
{
loadingPercent = value;
RaisePropertyChanged("LoadingPercent");
}
}
private void RaisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
public static class StaticClass
{
public static int Percentage { get; set; }
}
Here is a mistake:
private double loadingPercent;
public double LoadingPercent
{
get { return StaticClass.Percentage; }
set
{
loadingPercent = value;
RaisePropertyChanged("LoadingPercent");
}
}
You return in get the StaticClass.Percentage but you assign loadingPercent in set.
I am not sure why you need the static class after all, but if you want to ditch the viewmodel and bind directly to the static property, see here
I have recently discovered indexed properties. This looks like the perfect solution to the scenario in which the data I am working with would best be expressed in a collection, yet still needs to be implemented as a property that can be used in XAML databinding. I started with just a test of creating indexed properties, and I had no problems there, but I just don't seem to be able to get the binding to work.
Can anyone point out where I'm going wrong?
Here is the test class with a nested class to create the indexed property:
public class TestListProperty
{
public readonly ListProperty ListData;
//---------------------------
public class ListProperty : INotifyPropertyChanged
{
private List<string> m_data;
internal ListProperty()
{
m_data = new List<string>();
}
public string this[int index]
{
get
{
if ( m_data.Count > index )
{
return m_data[index];
}
else
{
return "Element not set for " + index.ToString();
}
}
set
{
if ( m_data.Count > index )
{
m_data[index] = value;
}
else
{
m_data.Insert( index, value );
}
OnPropertyChanged( "Item[]" );
Console.WriteLine( value );
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged( string propertyName )
{
PropertyChangedEventHandler handler = PropertyChanged;
if ( handler != null ) handler( this, new PropertyChangedEventArgs( propertyName ) );
}
}
//---------------------------
public TestListProperty()
{
ListData = new ListProperty();
}
}
Here is the XAML:
<Window x:Class="TestIndexedProperties.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>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBox Width="200" Grid.Row="0" Text="{Binding Path=ListData[0], Mode=TwoWay}" />
<TextBox Width="200" Grid.Row="1" Text="{Binding Path=ListData[1], Mode=TwoWay}" />
<TextBox Width="200" Grid.Row="2" Text="{Binding Path=ListData[2], Mode=TwoWay}" />
</Grid>
</Window>
And here is the Window code:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
TestListProperty test = new TestListProperty();
this.DataContext = test;
test.ListData[0] = "ABC";
test.ListData[1] = "Pleeez 2 wurk?";
test.ListData[2] = "Oh well";
}
}
Thanks for any and all help!
There's no mechanism in your code to indicate to the front-end when the list has changed, i.e. ListProperty is implementing INotifyPropertyChanged instead of INotifyCollectionChanged. The easiest fix would be to change m_data to type ObservableCollection and bind your XAML controls to that instead, although that probably defeats the purpose of what you're trying to do in the first place. The "proper" way is to subscribe to the CollectionChanged event and propagate the messages through:
public class TestListProperty
{
public ListProperty ListData { get; private set; }
//---------------------------
public class ListProperty : INotifyCollectionChanged
{
private ObservableCollection<string> m_data;
internal ListProperty()
{
m_data = new ObservableCollection<string>();
m_data.CollectionChanged += (s, e) =>
{
if (CollectionChanged != null)
CollectionChanged(s, e);
};
}
public string this[int index]
{
get
{
if (m_data.Count > index)
{
return m_data[index];
}
else
{
return "Element not set for " + index.ToString();
}
}
set
{
if (m_data.Count > index)
{
m_data[index] = value;
}
else
{
m_data.Insert(index, value);
}
Console.WriteLine(value);
}
}
public event NotifyCollectionChangedEventHandler CollectionChanged;
}
//---------------------------
public TestListProperty()
{
ListData = new ListProperty();
}
}
I am currently trying to follow MVVM in C# 4, but having troubles with the bindings working.
Starting from the bottom, here is my Property class that should take care of the property changed for XAML bindings:
namespace Visualizer.MVVM
{
public class Property<T> : DependencyObject, INotifyPropertyChanged
{
//private T _Value;
public T Value
{
get { return (T) GetValue(ValueProperty); }
set
{
SetValue(ValueProperty, value);
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged()
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs("Value"));
}
}
public Property(T val)
{
Value = val;
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(T), typeof(Property<T>));
}
}
My ViewModel for the control looks like this and is instantiated in MainWindow.xaml.cs:
public class CheckboxControlVM
{
public Property<bool> IsChecked { get; set; }
public Property<string> Name { get; set; }
public CheckboxControlVM(bool isChecked, string name)
{
IsChecked = new Property<bool>(isChecked);
Name = new Property<string>(name);
}
}
The control has no code-behind, so here is the XAML for it:
<UserControl x:Class="Visualizer.MVVM.Checkbox"
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"
xmlns:local="clr-namespace:Visualizer.MVVM"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<StackPanel Orientation="Horizontal" x:Name="LayoutRoot">
<CheckBox IsChecked="{Binding Path=IsChecked.Value, Mode=TwoWay}"/>
<TextBlock Text="{Binding Path=Name.Value, Mode=OneWay}"/>
</StackPanel>
</UserControl>
Finally, here is the binding in MainWindow.xaml:
<mvvm:Checkbox DataContext="{Binding Realtime}"/>
I have been stuck on this for a lot longer than I should be and am fairly certain its just a simple issue. Any ideas?
I don't quite get your objective of what you want to achieve with that property design. Normally I don't do that in WPF so I'm not quite sure whether this help or not.
Usually, I do implement INotifyPropertyChanged in ViewModel level, not in the attribute owned by VM. Example:
public class ViewModel : INotifyPropertyChanged{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Second, I do not use DependencyProperty unless I make a WPF user control. So I use private property and trigger the OnPropertyChanged with the property name.
private string _name;
public string Name{
set{
_name = value;
OnPropertyChanged("Name");
}
}
Last, in the XAML, I use binding with UpdateSourceTrigger=PropertyChanged.
<TextBlock Text="{Binding Path=Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
Maybe you can try to add UpdateSourceTrigger=PropertyChanged in your binding, but I am not sure if it will work.
try this
public class CheckboxControlVM
{
bool _isChecked = false;
string _name ;
public Property<bool> IsChecked { get { return _isChecked} set { _isChecked=value;} }
public Property<string> Name { get { return _name } set { _name =value;} }
public CheckboxControlVM(bool isChecked, string name)
{
_isChecked = isChecked;
_name = name;
IsChecked = new Property<bool>(_isChecked);
Name = new Property<string>(_name);
}
}