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.
Related
This question already has answers here:
WPF - MVVM: ComboBox value after SelectionChanged
(2 answers)
Closed 8 months ago.
I want to have the selected index of a combobox change based on code from the ViewModel. Is this possible?
This is how my combobox is set up:
<ComboBox x:Name="cmbModels" DisplayMemberPath="ModelItemTextbox"
SelectedItem="ItemNameTextbox" SelectionChanged="ModelSelectionChange"
ItemsSource="{Binding ModelComboList}">
</ComboBox>
Something else, my bindings don't work unless I have the SelectedItem set to "ItemNameTextbox". The Combobox is binded to an observableCollection.
private ObservableCollection<ModelComboListModel> _modelcombolist = new ObservableCollection<ModelComboListModel>();
public ObservableCollection<ModelComboListModel> ModelComboList
{
get { return _modelcombolist; }
set
{
_modelcombolist = value;
OnPropertyChanged("ModelComboList");
}
}
And the class:
public class ModelComboListModel
{
public string ItemName { get; set; }
public string ItemId { get; set; }
//public override string ToString()
//{
// return $"ID:{ModelItemId} | {ModelItemName}";
//}
public string ItemTextbox
{
get
{
return $"{ ItemId }: {ItemName}";
}
}
}
The list just contains items and their id's.
Is there a good trick for changing the selectedindex from the ViewModel? I can't find anything useful on google or here :(
You probably want to bind to the SelectedIndex property in your ComboBox. Then you can be strategic with your gets and sets in the view model to get the behavior you are looking for. In this example I made a TextBox that can be used to change the index of the ComboBox:
<StackPanel>
<ComboBox x:Name="cmbModels"
SelectedItem="{Binding SelectedItem, Mode=OneWay}"
SelectedIndex="{Binding SelectedItemIndex}"
ItemsSource="{Binding ModelComboList}"
Margin="5">
</ComboBox>
<TextBox Text="{Binding ItemSelect}"
Margin="5"/>
</StackPanel>
View Model:
internal class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChangedEventHandler? handler = PropertyChanged;
handler?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private int _selectedItemIndex = 0;
public int SelectedItemIndex
{
get => _selectedItemIndex;
set
{
_selectedItemIndex = value;
OnPropertyChanged();
}
}
public ObservableCollection<string> ModelComboList { get; } = new() { "Item", "Foo", "Bar" };// populate with more items as needed
public string SelectedItem => ModelComboList[_selectedItemIndex];
public string ItemSelect
{
get => _selectedItemIndex.ToString();
set
{
if (int.Parse(value) < 0 || int.Parse(value) > ModelComboList.Count)
SelectedItemIndex = 0;
else
SelectedItemIndex = int.Parse(value);
}
}
I have a TextBlock in XAML that's bound to a property called EditsWarning:
<TextBlock DockPanel.Dock="Top" Text="{Binding EditsWarning, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Style="{DynamicResource Esri_TextBlockRegular}" HorizontalAlignment="Left" FontSize="14" FontWeight="DemiBold" VerticalAlignment="Center" Margin="10,0,10,5" TextWrapping="WrapWithOverflow"/>
The Definition for the EditsWarning Property is here:
public string EditsWarning
{
get { return editsWarningMessage; }
set
{
SetProperty(ref editsWarningMessage, value, () => this.EditsWarning);
}
}
The EditsWarning Property is set to an instance of a class like this:
editsWarning = new OutstandingEditsTextBlock();
editsWarningMessage = editsWarning.EditsWarningMessage.ToString();
And the OutstandingEditsTextBlock class is here, and implements INotifyPropertyChanged
internal class OutstandingEditsTextBlock : INotifyPropertyChanged
{
private string editsWarning;
public OutstandingEditsTextBlock()
{
if (Project.Current.HasEdits)
{
this.editsWarning = "This session/version has outstanding edits.";
}
else
{
this.editsWarning = string.Empty;
}
}
public event PropertyChangedEventHandler PropertyChanged;
public string EditsWarningMessage
{
get { return this.editsWarning; }
set
{
this.editsWarning = value;
this.OnPropertyChanged("EditsWarningMessage");
}
}
public void OnPropertyChanged(string propertyName)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
I noticed that I can get it to display either value, however, I can never get it to update in the same debugging session. In fact, it looks like the setter for the public property is never hit.
Can someone please help me figure out what I'm doing wrong?
Thank you.
I have an ObservableCollection bound to a ListBox. Selecting an item in the list box populates a user control with it's own viewmodel based on the selected item. I am using a Linq to SQL DataContext for getting data from my model to the viewmodels.
The problem is that the displaymember for the listbox is bound to a property that combines two fields, a number and a date, for the item. The usercontrol allows the user to change the date, and I want that to be reflected in the list box immediately.
I initialize the collection and add in CollectionChanged and PropertyChanged handlers so that the collection is listening for the changes to properties within the collection:
public void FillReports()
{
if (oRpt != null) oRpt.Clear();
_oRpt = new ViewableCollection<Reportinformation>();
//oRpt.CollectionChanged += CollectionChanged; //<--Don't need this
foreach (Reportinformation rpt in _dataDc.Reportinformations.Where(x => x.ProjectID == CurrentPrj.ID).OrderByDescending(x => x.Reportnumber))
{
oRpt.Add(rpt);
}
}
private void CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e != null)
{
if (e.OldItems != null)
{
foreach (INotifyPropertyChanged rpt in e.OldItems)
{
rpt.PropertyChanged -= item_PropertyChanged;
}
}
if (e.NewItems != null)
{
foreach (INotifyPropertyChanged rpt in e.NewItems)
{
rpt.PropertyChanged += item_PropertyChanged;
}
}
}
}
private void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
string s = sender.GetType().ToString();
if(s.Contains("Reportinformation"))
RaisePropertyChangedEvent("oRpt"); //This line does get called when I change the date
else if (s.Contains("Observation"))
{
RaisePropertyChangedEvent("oObs");
RaisePropertyChangedEvent("oObsByDiv");
}
}
The date gets changed correctly and the change persists and is written back to the database, but the change does not reflect in the listbox unless I actually change the collection (which happens when I switch jobs on another control in the same window as the listbox). The line in my property changed handler raises the change event for "oRpt" which is the observable collection bound to the ListBox, and changing the date does call the handler as verified with the debugger:
<ListBox x:Name="lsbReports" ItemsSource="{Binding oRpt}" DisplayMemberPath="ReportLabel" SelectedItem="{Binding CurrentRpt}"
Grid.Row="1" Grid.Column="0" Height="170" VerticalAlignment="Bottom" BorderBrush="{x:Null}" Margin="0,0,5,0"/>
But it seems that simply raising that change doesn't actually trigger the view to refresh the "names" of the items in the listbox. I have also tried to Raise for the ReportLabel bound to the DisplayMemberPath, but that doesn't work (worth a try though). I'm not sure where to go from here, as I think it's bad practice to reload the oRpt collection based on changing the date (therefore the name) of one of the actual items as I expect this database to grow fairly quickly.
Here is the Reportinformation extension class (this is an auto generated LinqToSQL class, so just my part is below):
public partial class Reportinformation // : ViewModelBase <-- take this out INPC already hooked up
{
public ViewableCollection<Person> lNamesPresent { get; set; }
public string ShortDate
{
get
{
DateTime d = (DateTime)Reportdate;
return d.ToShortDateString();
}
set
{
DateTime d = DateTime.Parse(value);
if (d != Reportdate)
{
Reportdate = DateTime.Parse(d.ToShortDateString());
SendPropertyChanged("ShortDate");//This works and uses the LinqToSQL call not my ViewModelBase call
SendPropertyChanged("ReportLabel"); //use the LinqToSQL call
//RaisePropertyChangedEvent("ReportLabel"); //<--This doesn't work
}
}
}
public string ReportLabel
{
get
{
return string.Format("{0} - {1}", Reportnumber, ShortDate);
}
}
public void Refresh()
{
RaisePropertyChangedEvent("oRpt");
}
public string RolledNamesString
{
get
{
if (lNamesPresent == null) return null;
return string.Join("|",lNamesPresent.Where(x=>x.Name!= "Present on Site Walk").Select(x=>x.Name).ToArray());
}
}
}
ANSWER
So my mistake was that I was adding to the LinqToSQL partial classes, and was using my ViewModelBase there which reimplements all of the INPC stuff over top of the autogenerated partial class. I undid that, and just use the INPC from the autogenerated designer stuff and it all works as expected. Thanks to SledgeHammer for chatting and making me rethink all of this!
You can solve this one of two ways. Either your ReportInformation class needs to implement INotifyPropertyChanged and raise the property changed events for the ReportLabel property whenever it changes:
public class ReportInformation : INotifyPropertyChanged
{
private int _numberField;
private DateTime _dateField;
public int NumberField
{
get => _numberField;
set
{
if (_numberField != value)
{
_numberField = value;
RaisePropertyChanged();
RaisePropertyChanged(nameof(ReportLabel));
}
}
}
public DateTime DateField
{
get => _dateField;
set
{
if (_dateField != value)
{
_dateField = value;
RaisePropertyChanged();
RaisePropertyChanged(nameof(ReportLabel));
}
}
}
public string ReportLabel => $"{NumberField}: {DateField}";
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void RaisePropertyChanged([CallerMemberName]string name = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
OR, you can use in your ListBox an ItemTemplate rather than DisplayMemberPath like so:
<ListBox x:Name="lsbReports"
ItemsSource="{Binding oRpt}"
SelectedItem="{Binding CurrentRpt}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding NumberField}"/>
<TextBlock Text=": "/>
<TextBlock Text="{Binding DateField}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I've read all over the place, that binding is doable in WPF to Interfaces, but I'm having a heck of a time actually getting any traction with it. I'm using EF Core also, if it helps you ready my code. The ComboBox fills with data, so the bind of the data works, but SelectedItem fails to bind, and the text within the selected item shows blank.
I don't get how the following, binds to the object that implements the interface.
The XAML for ComboBox:
<ComboBox Height="23" x:Name="cbJumpList" Width="177" Margin="2" HorizontalAlignment="Left"
IsEditable="False"
DisplayMemberPath="Name"
SelectedItem="{Binding Path=(model:IData.SelectedJumpList), Mode=TwoWay}"
/>
MainWindow.xaml.cs:
protected IData DB { get; private set; }
public MainWindow()
{
InitializeComponent();
DB = new Data.DataSQLite(true);
DB.Bind_JumpLists_ItemsSource(cbJumpList);
}
IData.cs:
public interface IData : IDisposable, INotifyPropertyChanged
{
void Bind_JumpLists_ItemsSource(ItemsControl control);
IJumpList First_JumpList();
IJumpList SelectedJumpList { get; set; } // TwoWay Binding
}
IJumpList.cs
public interface IJumpList
{
long JumpListId { get; set; }
string Name { get; set; }
}
Then within the implemented object (Data.DataSQLite):
public void Bind_JumpLists_ItemsSource(ItemsControl control)
{
control.ItemsSource = null;
db.JumpLists.ToList();
control.ItemsSource = db.JumpLists.Local;
control.Tag = db.JumpLists.Local;
SelectedJumpList = db.JumpLists.FirstOrDefault();
}
public IJumpList SelectedJumpList
{
get { return _SelectedJumpList; }
set
{
_SelectedJumpList = value;
NotifyPropertyChanged();
}
}
IJumpList _SelectedJumpList;
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
I should add, the PropertyChanged event remains null.
The SelectedItem property of a ComboBox is supposed to be bound to a property and not to a type. For the binding to work you should also set the DataContext of the ComboBox to an instance of the type where this property is defined.
Try this:
<ComboBox Height="23" x:Name="cbJumpList" Width="177" Margin="2" HorizontalAlignment="Left"
IsEditable="False"
DisplayMemberPath="Name"
SelectedItem="{Binding SelectedJumpList}" />
public void Bind_JumpLists_ItemsSource(ItemsControl control)
{
db.JumpLists.ToList();
control.DataContext = this;
control.ItemsSource = db.JumpLists.Local;
control.Tag = db.JumpLists.Local;
SelectedJumpList = db.JumpLists.FirstOrDefault();
}
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);
}
}