I'm selecting an item from a combobox to filter a listview of items. The items contain values, and are displayed in a View depending on the filter selection.
<ComboBox Name="YearComboBox" ItemsSource="{x:Bind StudentEnrollment.Years, Mode=OneWay}" SelectedValue="{x:Bind StudentEnrollment.SelectedYear, Mode=TwoWay}”/>
<ListView ItemsSource="{x:Bind StudentEnrollment.FilteredStudentEnrollments, Mode=OneWay}" SelectedIndex="{x:Bind StudentEnrollment.SelectedIndex, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="viewModels:StudentViewModel" >
<StackPanel Orientation="Horizontal">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{x:Bind Score01, Mode=OneWay}"/>
</Grid>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
public class StudentEnrollmentViewModel : NotificationBase
{
StudentEnrollment StudentEnrollment;
public StudentEnrollmentViewModel()
{
}
private ObservableCollection<StudentEnrollmentViewModel> _StudentEnrollments;
public ObservableCollection<StudentEnrollmentViewModel> StudentEnrollments
{
get { return _StudentEnrollments; }
set
{
SetProperty(ref _StudentEnrollments, value);
}
}
private ObservableCollection<StudentEnrollmentViewModel> _FilteredStudentEnrollments;
public ObservableCollection<StudentEnrollmentViewModel> FilteredStudentEnrollments
{
get { return _FilteredStudentEnrollments; }
set
{
SetProperty(ref _FilteredStudentEnrollments, value);
}
}
private string _selectedYear;
public string SelectedYear
{
get
{
return _selectedYear;
}
set { SetProperty(ref _selectedYear, value);
{ RaisePropertyChanged(SelectedYear); }
RefreshFilteredStudentEnrollmentData(); }
}
private double _Score01;
public double Score01
{
get
{
_Score01 = FilteredStudentEnrollments.Where(y => y.Year == SelectedYear).Select(s => s.Score01).Sum();
return _Score01;
}
}
private void RefreshFilteredStudentEnrollmentData()
{
var se = from seobjs in StudentEnrollments
where seobjs.Year == SelectedYear
select seobjs;
if (FilteredStudentEnrollments.Count == se.Count()) || FilteredStudentEnrollments == null
return;
FilteredStudentEnrollments = new ObservableCollection<StudentEnrollmentViewModel>(se);
}
As expected I can sum the filtered values, and display the total in a TextBlock text property per listview column when the page loads.
<TextBlock Text="{x:Bind StudentEnrollment.Score01, Mode=OneWay}"/>
The issue's the Textblock UI does not update/display the changing property value in the viewmodel via a combobox selection. I sure I'm missing something, or have some logic backwards, hoping some fresh eyes can help.
I am not sure which class is which, because there seems to be some mix up between StudentEnrollmentViewModel and StudentViewModel, but I think I see the reason for the problem.
The Score01 property is a get-only property (actually you can get rid of the _Score01 field and just return the value). Its value is dependent on the FilteredStudentEnrollments and SelectedYear properties. But the UI does not know that so you have to notify it when these properties change that it should bind a new value of Score01 as well:
private ObservableCollection<StudentEnrollmentViewModel> _FilteredStudentEnrollments;
public ObservableCollection<StudentEnrollmentViewModel> FilteredStudentEnrollments
{
get { return _FilteredStudentEnrollments; }
set
{
SetProperty(ref _FilteredStudentEnrollments, value);
RaisePropertyChanged("Score01");
}
}
private string _selectedYear;
public string SelectedYear
{
get
{
return _selectedYear;
}
set
{
SetProperty(ref _selectedYear, value);
RaisePropertyChanged("Score01");
RefreshFilteredStudentEnrollmentData();
}
}
public double Score01
{
get
{
return FilteredStudentEnrollments.
Where(y => y.Year == SelectedYear).Select(s => s.Score01).Sum();
}
}
Notice I have changed the RaisePropertyChanged call to pass in "Score01", as the method expects the name of the property, whereas you were previously passing the value of the SelectedYear property, which meant the PropertyChanged event executed for something like 2018 instead of the property itself. Also note that SetProperty method internally calls RaisePropertyChanged for SelectedYear property as well.
Related
I am new to C#/WPF. There is a view with one button defined, when the view is initialized, buttons will display a set of reason codes got from DataContext (viewmodel), once any button is clicked, the code on it will be saved and passed forward for next processing.
Q: The text on buttons are totally empty, but the clicked code can be captured, so where the problem is about binding? Thanks.
XAML:
<Button x:Name="btnReason" Command="{Binding DataContext.SelectCommand, RelativeSource={RelativeSource AncestorType=v:View, Mode=FindAncestor}}" CommandParameter="{Binding}" Width="190" Height="190" >
<Border Background="Transparent">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock x:Name="Reason" Grid.Row="0" Text="{Binding ?????}" TextWrapping="Wrap" />
</Grid>
</Border>
</Button>
The code on C#:
public class ReasonsViewModel : ViewModel
{
private IEnumerable<string> m_Names;
public IEnumerable<string> Names
{
get { return m_Names; }
set
{
if (m_Names != value)
{
m_Names = value;
OnPropertyChanged(() => Names);
}
}
}
private string m_SelectedName;
public string SelectedName
{
get { return m_SelectedName; }
set
{
if (m_SelectedName != value)
{
m_SelectedName = value;
OnPropertyChanged(() => SelectedName);
}
}
}
public DelegateCommand SelectCommand { get; private set; }
public ReasonsViewModel()
{
SelectCommand = new DelegateCommand(p => SelectCommandExecute(p));
}
private bool m_Processing;
private void SelectCommandExecute(object item)
{
if (m_Processing) return;
try
{
m_Processing = true;
var name = item as string;
if (name == null) return;
SelectedName = name;
}
finally
{
m_Processing = false;
}
}
}
If I understood your question correctly than your property text in your TextBlock should be bound to SelectedName.
The problem is that your CommandParameter is bound to DataContext. That's what an empty {Binding} statement bounds to. This means your command handler always returns after the null check.
I also suggest that you change your Names proeprty from IEnumerable<string> to ObservableCollection<string>.
ObservableCollection raises events on any additions or removalof items inside and WPF components can bind to these events.
So many examples found and none fit! My list box is a list of Result objects. Results can be checked or unchecked in a listbox to mark them as 'Allowed to 'transmit.
<ListBox
x:Name="FileListBox"
ItemsSource="{Binding TestResults}"
ItemTemplate="{StaticResource FileListTemplate}"
SelectionMode="Single"
SelectedItem="{Binding FileListSelected}"
Background="#FFFFFBE2" />
The FileListTemplate
<DataTemplate x:Key="FileListTemplate">
<Grid HorizontalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=".5*" />
<ColumnDefinition Width=".3*" />
<ColumnDefinition Width=".2*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Text="{Binding FileName}" />
<TextBlock Grid.Column="1"
Text="Machine">
</TextBlock>
<CheckBox x:Name="UploadOK"
Grid.Column="2"
HorizontalAlignment="Right"
IsChecked="{Binding CanUpload, Mode=TwoWay}" />
</Grid>
</DataTemplate>
I took out a lot of formatting code to reduce the clutter. So when the check box is checked (or un checked) I need to set a boolean on the object to true or false. But I do not want the ListItem selected just because the checkbox is selected. When the ListItem is selected something else happens. Here is the code for that.
public TestResult FileListSelected
{
get
{
return selectedItem;
}
set
{
if (value == selectedItem)
return;
selectedItem = value;
if (!Workspaces.Any(p => p.DisplayName == value.FileName))
{
this.DisplayTestResult(value as TestResult);
}
base.RaisePropertyChanged("FileListSelected");
}
}
And here is the code I bound to for the Checkbox (although it didn't work).
public bool CanUpload
{
get { return selectedItem.CanUpload; }
set
{
selectedItem.CanUpload = value;
}
}
I appreciate you looking at this.
Internal Class TestResult
{
...
private bool _canUpload;
public bool CanUpload
{
get { return _canUpload; }
set
{
_canUpload = value;
base.RaisePropertyChanged("CanUpload");
}
}
}
When working with MVVM always check for the following:
Add using System.ComponentModel; to your ViewModelClass
Inherit from INotifyPropertyChanged
Always check your DataContext and see the Output Window for BindingErrors
Create Bindings like this:
Example Property:
public string Example
{
get { return _example; }
set
{
_example= value;
OnPropertyChanged();
}
}
this will call OnPropertyChanged automatically every time a new value is assigned (not updated automaticaly once it changes from some other location!)
Make sure your Implementation of INotifyPropertyChanged looks like this:
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
for that you also need using System.Runtime.CompilerServices;
Other options to get your code working:
Your TestResults sould be an ObservableCollection<TestResult>
TestResult should have a property for CanUpload and FileName and inherit from INotifyPropertyChanged
Then on your MainViewModel for example on and ButtonClick your can get the selected files like this:
private List<string> GetSelectedFiles()
{
return TestResults.Where(result => result.CanUpload == true).Select(r => r.FileName).ToList());
}
Note:
FileListSelected is a Property of your ListBox's DataContext which is different to the DataContext of an entry (or at least should be).
FileListSelected will then return the selected Item of your ItemsSource.
Maybe you can comment on this problem with the row selection/checkbox check and add some detail so I can help you more.
EDIT: Notify MainWindowViewModel about CheckBox State Changes:
I see two possible approaches here:
USING EVENT
Add this to your TestResult class:
public delegate void CheckBoxStateChangedHandler(object sender, CheckBoxStateChangedEventArgs e);
public event CheckBoxStateChangedHandler CheckBoxStateChanged;
public class CheckBoxStateChangedEventArgs
{
bool CheckBoxChecked { get; set; }
}
Make sure that on creation of a new TestResult in your MainViewModel you subscribe to that event;
testResult.CheckBoxStateChanged += CheckBox_StateChanged;
Handle what you want to do once the state is changed in CheckBox_StateChanged. Note that the argument e contains the boolean (Checked) and the corresponding TestResult as the sender.
You simply invoke your new Event in the Setter of your CheckBox.Checked Binding:
public bool Checked
{
get { return _checked; }
set
{
_checked = value;
OnPropertyChanged();
CheckBoxStateChanged.Invoke(this, new CheckBoxStateChangedEventArgs() { CheckBoxChecked = value })
}
}
CALL METHOD ON MAINWINDOWVIEWMODEL
for that you need o create a static object of your MainWindowViewModel (in your MainViewModel) - don't forget to assigne a value once you create your MainWindowViewModel.
public static MainViewModel Instance { get; set; }
then simply add a public Method as you need:
public void CheckBoxValueChanged(bool value, TestResult result)
{
//Do whatever
}
you can also call in from the same spot as the event from above is invoked.
public bool Checked
{
get { return _checked; }
set
{
_checked = value;
OnPropertyChanged();
MainWindowViewModel.Instance.CheckBoxValueChanged(value, this);
}
}
For the purpose of code reuse, I am attempting to bind a ComboBox ItemsSource to an enumerable of enum values defined in a viewmodel. (I am aware of the strategies for binding directly to the enum, but in order to achieve code reuse I need to bind to an enumerable.) On viewmodel construction, I set the selected item to the first value of the enumerable. When the UI first launches, however, the combobox loads with validation error:
Value '' could not be converted.
This error does not occur when I use the same XAML to bind to an enumerable of classes. After I select an enum value, I get no more validation errors and the UI works as intended. How do I avoid this error and get the combobox to display the selected item on startup?
The code details... I have a service implementing IAcquire<T> which returns an enumerable of enum values:
public interface IAcquire<T>
{
IReactiveList<T> Items { get; }
}
My viewmodel inheritance looks something like this:
class GranularitySelectionViewModel : ChartFilterSelectionBase<DataGranularity>
{
public GranularitySelectionViewModel([NotNull] IAcquire<DataGranularity> service)
: base(service, "Granularity")
{}
}
class ChartFilterSelectionBase<T> : SelectionViewModelBase
{
private readonly IAcquire<T> _service;
internal ChartFilterSelectionBase([NotNull] IAcquire<T> service, string label)
:base(label)
{
foreach (var value in service.Items)
{
Items.Add(value);
}
SelectedItem = Items.FirstOrDefault();
}
private readonly IReactiveList<T> _items = new ReactiveList<T>();
public new IReactiveList<T> Items
{
get { return _items; }
}
private T _selectedItem;
public new T SelectedItem
{
get { return _selectedItem; }
set { SetProperty(ref _selectedItem, value); }
}
}
public class SelectionBaseViewModel
{
protected SelectionBaseViewModel([NotNull] string label )
{
if (label == null) throw new ArgumentNullException("label");
_label = label;
}
private readonly string _label;
public string Label
{
get { return _label; }
}
//Placeholder to be overridden in derived class.
public object SelectedItem { get; set; }
//Placeholder to be overridden in derived class.
public IReactiveList<object> Items { get; private set; }
}
The XAML is as follows:
<DataTemplate DataType="{x:Type viewModels:SelectionBaseViewModel}">
<StackPanel Orientation="Vertical">
<Label Content="{Binding Label}" ContentStringFormat="{}{0}:" Margin="5,5,5,0"/>
<ComboBox Margin="5,0,5,5" ItemsSource="{Binding Items, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" IsSynchronizedWithCurrentItem="True"
SelectedItem="{Binding SelectedItem, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" BorderThickness="1" BorderBrush="White">
</ComboBox>
</StackPanel>
</DataTemplate>
I have a colection and a ListView to which I have binded a collection of objects:
<ListView ItemsSource="{Binding Levels}"... />
Here is a Levels collection:
private ObservableCollection<Level> _levels;
public ObservableCollection<Level> Levels
{
get { return _levels; }
set { SetProperty(ref _levels, value); }
}
And here is a Level class:
public class Level : BindableBase
{
private double _value;
public double Value
{
get { return _value; }
set { SetProperty(ref _value, value); }
}
private bool _isChecked;
public bool IsChecked
{
get { return _isChecked; }
set { SetProperty(ref _isChecked, value); }
}
public SolidColorBrush ForegroundColor
{
get { return IsChecked ? new SolidColorBrush(Colors.Yellow) : new SolidColorBrush(Colors.BlueViolet); }
}
}
If I add element in the collection, the new element will be displayed in the ListView, but if I change the existing element of collection I can not see any change:
private void LvLevels_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (LvLevels.SelectedItem != null)
{
selectedLevel = (Level)LvLevels.SelectedItem;
foreach (var l in viewModel.Levels)
{
if (l.Value == selectedLevel.Value)
l.IsChecked = true; // it doesn't work
else
l.IsChecked = false;
}
// it works
// viewModel.Levels.Add(new Level { Value = 10, IsChecked = true});
}
Why and how can I fix it ?
Update
Here is my ItemTemplate:
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image Grid.Column="0"
Source="../Assets/icons/зоны.png"
Margin="10 0 0 0"/>
<TextBlock x:Name="tblock" Text="{Binding Value}" Grid.Column="1" FontSize="30"
Foreground="{Binding ForegroundColor}" />
<!-- Style="{StaticResource ZoneButtonText}" -->
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
If you expect the ForegroundColor to change (that's a guess), you need to make sure the WPF components know it has changed. You need to send a change notification for ForegroundColor when IsChecked changes, too.
Oh, and don't compare doubles using ==. Doubles are floating point variables.
I've been having some trouble getting a listbox to correctly bind to a collection.
I'll give the framework code, then explain what I want it to do.
XAML Markup:
<ListBox DataContext="{Binding Foos, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ItemsSource="{Binding}" IsSynchronizedWithCurrentItem="True"
SelectedItem="{Binding Main.SelectedFoo, Mode=TwoWay,
Source={StaticResource Locator},
UpdateSourceTrigger=PropertyChanged}"
SelectedValue="{Binding Main.SelectedFoo, Source={StaticResource Locator}}"/>
<ListBox ItemsSource="{Binding Main.SelectedFoo.Bars}" SelectedItem="{Binding Main.SelectedBar}" >
<ListBox.ItemTemplate>
<DataTemplate>
<Grid HorizontalAlignment="Right">
<!-- The binding requires "{Binding .}" because a path must be explicitly set for Two-Way binding,
even though {Binding .} is supposed to be identical to {Binding} -->
<TextBox Text="{Binding Path=. , UpdateSourceTrigger=PropertyChanged}" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
C# ViewModel:
private ObservableCollection<Foo> _barList = new ObservableCollection<Foo>();
private const string BardListPN = "FooList";
public ObservableCollection<Foo> FooList
{
get { return _fooList; }
set
{
if (_fooList == value)
{
return;
}
var oldValue = _fooList;
_fooList = value;
RaisePropertyChanged(FooListPN);
}
}
private Foo _selectedFoo;
private const string SelectedFooPN = "SelectedFoo";
public Foo SelectedFoo
{
get { return _selectedFoo; }
set
{
if (_selectedFoo == value)
{
return;
}
var oldValue = _selectedFoo;
_selectedFoo = value;
// Update bindings, no broadcast
RaisePropertyChanged(SelectedFooPN);
}
}
public const string SelectedBarPN = "SelectedBar";
private string _selectedBar = "";
public string SelectedBar
{
get
{
return _selectedBar;
}
set
{
if (_selectedBar == value)
{
return;
}
var oldValue = _selectedBar;
_selectedBar = value;
// Update bindings, no broadcast
RaisePropertyChanged(SelectedBarPN);
}
}
C# Model:
public class Foo
{
public ICollection<string> Bars
{
get { return _bars; }
set
{
_bars= value;
NotifyPropertyChanged("Bars");
// snipped obvious INotifyPropertyChanged boilerplate code
}
}
}
My problem is that any changes to the textboxes for the strings in the Bar collection aren't set. When the selected Foo changes to a different Foo and back, the original Bars are displayed.
Could someone tell me what I'm doing wrong? This seems like it should be much more simple. Thanks!
Update: I've changed the code as per Tri Q's suggestion, but the changes made to the textbox aren't reflected in the property itself. Any ideas?
Your Foo model class I take has been simplified for this example, but the omitted code could be the culprit of your problem. Let me explain.
Foo also needs to implement INotifyPropertyChanged to let the Listbox know when you have initialized the Bars collection and this most definitely depends on when you are initializing it.
Say you initialize Bars in Foo's constructor will cause the Listbox ItemsSource to bind to a valid Bars collection.
public Foo()
{
Bars = new ObservableCollection<string>();
...
}
Buut if you did something like this, the Listbox will not know that the Bars collection has been initialized and will not update it's source...
public Foo SelectedFoo
{
get { return _selectedFoo; }
set
{
if (_selectedFoo == value)
{
return;
}
var oldValue = _selectedFoo;
_selectedFoo = value;
// Update bindings, no broadcast
RaisePropertyChanged(SelectedFooPN);
if(_selectedFoo.Bars == null)
{
_selectedFoo.Bars = new ObservableCollection<string>();
// ...
}
}
}
Also here are a few things you might want to revise in your XAML.
Firstly, binding of the Textbox is TwoWay by default, so you do not need to set the Mode or the Path.
<TextBox Text="{Binding UpdateSourceTrigger=PropertyChanged}" />
Secondly, it makes no sense to set Mode="TwoWay" for ItemsSource. ItemsSource="{Binding Main.SelectedFoo.Bars, Mode=TwoWay}"
Finally, you don't need to set the DataType for your DataTemplate. DataType="{x:Type System:String}"