I'm using the MVVM pattern and have a databound Listbox that isn't updating completely.
There is a modelview that contains an Observable collection of Machines which is bound to the list:
<ListBox Name="MachinesList"
Height="300"
Width="290"
DataContext="{Binding Path=AllMachines}"
SelectionMode="Single"
ItemsSource="{Binding}" SelectionChanged="MachinesList_SelectionChanged"
HorizontalAlignment="Right"
>
The collection AllMachines Contains an observable collection of MachineModelViews which are in turn bound to a MachineView that presents the name and location of the machine:
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Label Name="NameLabel" Content="{Binding Path=Name}" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Width="50" />
<Label Content="Location:" Width="120"
HorizontalAlignment="Right"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Target="{Binding ElementName=locationLabel}"
/>
<Label Content="{Binding Path=Location.Name}" Name="locationLabel" HorizontalContentAlignment="Center" Width="60"/>
</StackPanel>
The problem:
When values are added to the collection things update okay. When a machine is deleted however only the Label bound to Location.Name updates the other two remain in the listbox. I've confirmed that the collection is actually updating and removing the MachineModelView Correctly but some how the label with it's name and the "label label" with "Location:" continues to exist until the application is restarted:
before:
after delete:
after app restart:
The delete button calls a command which removes the item from the private member that backs the AllMachines property and from the repository (which ultimately plugs into a database via Entity Framework):
RelayCommand _deleteCommand;
public ICommand DeleteCommand
{
get
{
if (_deleteCommand == null)
{
_deleteCommand = new RelayCommand(
param => this.Delete(),
null
);
}
return _deleteCommand;
}
}
void Delete()
{
if (_selectedMachine != null && _machineRepository.GetMachines().
Where(i => i.Name == _selectedMachine.Name).Count() > 0)
{
_machineRepository.RemoveMachine(_machineRepository.GetMachines().
Where(i => i.Name == _selectedMachine.Name).First());
_allMachines.Remove(_selectedMachine);
}
}
Note: There can only be one item with a name in AllMachines (this is handled by the add logic in the repository and command itself) so removing the "First" one should be fine.
The AllMachines property:
public ObservableCollection<MachineViewModel> AllMachines
{
get
{
if(_allMachines == null)
{
List<MachineViewModel> all = (from mach in _machineRepository.GetMachines()
select new MachineViewModel(mach, _machineRepository)).ToList();
_allMachines = new ObservableCollection<MachineViewModel>(all);
}
return _allMachines;
}
private set
{
_allMachines = value;
}
}
The selection changed event handler:
private void MachinesList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count > 0 && e.AddedItems[0] is MachineViewModel)
((MachinesViewModel)this.DataContext).SelectedMachine = (MachineViewModel)e.AddedItems[0];
}
If 'AllMachines is your actual observable collection, I would bind the itemssource to that instead of binding it to ur datacontext, and then bind ur viewmodel as your datacontext. It may also be helpful to call the updatesourcetrigger after your itemssource bind.
DataContext="{Binding Path=YourViewModel}"
SelectionMode="Single"
ItemsSource="{Binding AllMachines, UpdateSourceTrigger="PropertyChanged"}"
And you can use that updatesourcetrigger on some of your labels as well where you are binding. Other than that, you xaml looks ok, hard for anyone to really say until they see everything.
_machineRepository.RemoveMachine(_machineRepository.GetMachines().
Where(i => i.Name == _selectedMachine.Name).First());
AllMachines.Remove(_selectedMachine);
Instead of field _allMachines.Remove remove directly from the property AllMachines like
AllMachines.Remove
I hope this will help.
if you code the way you said here, then all should work. pls post the missing code pieces:
your AllMaschines Property
your MachinesList_SelectionChanged event <-- when doing MVVM you almost not need this
pls debug your Delete() method just to be sure that: _allMachines.Remove(_selectedMachine); is hit and the machine is really removed from your collection.
I found my problem, really stupid: I have an event that fires when the repository updates. Before I only had an add command and I was adding the ability to delete when this problem came up. Turns out the event was being handled by the MachinesModelView to update it's internal variable that is the source for the AllMachines property:
void OnMachineAddedToRepository(object sender, MachineAddedOrRemovedEventArgs e)
{
var viewModel = new MachineViewModel(e.NewMachine, _machineRepository);
this.AllMachines.Add(viewModel);
}
I modified the event arguments to toggle between add and delete but forgot to update the event handler. Now it does work:
void OnMachineAddedOrRemovedFromRepository(object sender, MachineAddedOrRemovedEventArgs e)
{
if (e.Added)
{
var viewModel = new MachineViewModel(e.NewMachine, _machineRepository);
this.AllMachines.Add(viewModel);
}
else if (AllMachines.Where(i => i.Name == e.NewMachine.Name).Count() > 0)
AllMachines.Remove(AllMachines.Where(i => i.Name == e.NewMachine.Name).First());
}
What made this so hard to track down is that the extra item only lived very briefly until the AllMachines.Remove part of the Delete command ran. So checking the pre-delete count, and the post delete count looked right it was the guts in the middle when the item is being deleted from the repository that fired off an event that added the item back and left it in this weird state. Kind of odd that it consistently only had the location part of the MachineModelView floating around. I now just let the event handler add or remove the items from the _allMachines variable and don't touch it explicitly in the delete command (the delete command just deletes it from the repository and lets the UI variables catch up in the fraction of a second it takes for the event to trigger). Thanks a bunch guys for your help.
Related
In my WPF application, I am developing a fairly straightforward page that allows either creating a new object or choosing one from a combo box, then editing the object.
One of the parts of the object that is editable is a related database table in a one-to-many relationship, so for that piece I used a DataGrid. The DataGrid itself has a data-bound ComboBox column, as you can see here:
<DataGrid AutoGenerateColumns="False" EnableRowVirtualization="True"
CanUserAddRows="False" CanUserDeleteRows="True"
ItemsSource="{Binding Path=No.Lower_Assy}"
DataGridCell.Selected="dgAssy_GotFocus">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Number & Type">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding Path=DataContext.ComboSource, RelativeSource={RelativeSource AncestorType=Page}}"
SelectedValuePath="bwk_No"
SelectedValue="{Binding Path=fwf_Higher_N, ValidatesOnDataErrors=True, ValidatesOnExceptions=True, NotifyOnValidationError=True, UpdateSourceTrigger=PropertyChanged}">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Number}"/>
<TextBlock Text="{Binding Path=Type}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<!-- other text columns omitted -->
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="Delete" Click="btnDeleteHigherAssy_Click" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
Code behind:
private void dgAssy_GotFocus(object sender, RoutedEventArgs e)
{
if (e.OriginalSource.GetType() == typeof(DataGridCell))
{
// Starts the edit on the row
DataGrid grd = (DataGrid)sender;
grd.BeginEdit(e);
}
}
And for the save button:
private void btnSave_Click(object sender, RoutedEventArgs e)
{
if (CanUserEdit())
{
if (string.IsNullOrWhiteSpace(model.Data.Error))
{
repo.Save(model.Data);
StatusText = STATUS_SAVED;
model.CanSave = false;
// This is the data source for the main combo box on the page
model.ComboSource = repo.GetData();
// Set the combo box's selected item, in case this is a new object.
// cboNo is the main combo box on the page which allows selecting
// an object to edit
// Apparently setting SelectedItem directly doesn't work on a databound combo box
int index = model.ComboSource.ToList().FindIndex(x => x.bwk_No == model.Data.bwk_No);
cboNo.SelectedIndex = index;
}
else
{
MessageBox.Show("Invalid data:\n" + model.Data.Error, "Cannot save");
}
}
}
The problem
When I choose an item from the combo box in the data grid, it seems to work until I click on the save button. Then two things happen:
The combo box's selected item is set to null, blanking out the combo box.
As a result of (1), the save button is re-enabled because the data has changed. (The save button is bound to model.CanSave, which as you can see is set to false in the button handler; it is set to true by a property change event handler if there are no data errors.)
Why is it being reset? I've followed the code flow closely and can see the property change event for the combo box's backing field (fwf_Higher_N) being handled, and it appears to somehow come from the line model.ComboSource = repo.GetData();, but the stack only shows [external code] and I don't see why that line would modify an existing object.
The model class
// Names have been changed to protect the innocent
private class MyDataViewModel : INotifyPropertyChanged
{
private DbData _Data;
public DbData Data
{
get { return _Data; }
set
{
_Data = value;
OnPropertyChanged("Data");
}
}
private IQueryable<MyComboModel> _ComboSource;
public IQueryable<MyComboModel> ComboSource {
get { return _ComboSource; }
set
{
_ComboSource = value;
OnPropertyChanged("ComboSource");
}
}
private bool _CanSave;
public bool CanSave
{
get { return _CanSave; }
set
{
_CanSave = value;
OnPropertyChanged("CanSave");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
Your description of what is going on and your markup doesn't quite match. I'm going to make some assumptions, such as that Page.DataContext is an instance of MyDataViewModel.
I'm sorry to say it, but a SSCCE would do wonders here. I strongly suggest when anyone gets into situations where they are elbow deep in code they don't quite understand that they break out what they are attempting to do and create a minimal prototype that either exhibits the same behavior, or that helps you learn what's going wrong. I've made 500+ prototypes in the past five years.
As for this situation, you refer to a ComboBox named cboNo in btnSave_Click, but I don't see that in the xaml. This ComboBox's ItemSource appears to be bound to MyDataViewModel.ComboSource.
In addition, all ComboBoxes in the DataGrid also appear to be bound to the model's ComboSource. And, in the button handler event, you change what is in the property:
// This is the data source for the main combo box on the page
model.ComboSource = repo.GetData();
This fires PropertyChanged, and every ComboBox bound to this property will be updated. That means not only cboNo but also every ComboBox in the DataGrid.
It is expected behavior that, when ComboBox.ItemsSource changes, if ComboBox.SelectedItem is not contained within the items source, that SelectedItem is nulled out.
I just spun up a prototype (501+) and it appears that if the IEnumerable that the ComboBox is bound to changes, but the elements in the IEnumerable do not, then SelectedItem is not nulled out.
var temp = combo.ItemsSource.OfType<object>().ToArray();
combo.ItemsSource = temp;
So, within the btnSave_Click event handler, you change this ItemsSource, which probably does not have the same instances that are already in the combo, thus nulling out SelectedItem for all ComboBoxes bound to this property, and then only update cboNo's SelectedIndex.
Now, as for what to do about it...
Well, not sure. From the rest of your code, it appears you need to do some more codebehind work to make sure only the necessary ComboBoxes have their sources updated...
I am having the hardest time figuring out a way to solve a problem I am having with databinding on a slider and a textbox.
The setup:
the current value of the slider is displayed inside of the textbox. When the user drags the slider the value is reflected inside the textbox. The user can choose to drag the slider and release to the value he chooses, click anywhere on the slider track to set the value or enter the value manually in the texbox. In the last case, the value entered in the textbox should update the slider position.
The texbox is two way bound to a datacontext property while the slider is one way bound to the same property. When the user slides or click on the slider tracker, I use the dragcompleted event of the slider to notify the datacontext of the modification. When the user clicks on the tracker on the other hand I use the OnValueChanged event of the slider to notify the datacontext (and use a flag to ensure the OnValueChanged was not triggered by a slider move)
The problem: The OnValueChanged event fires even when initializing the slider value with the binding value so I cannot figure out whether the value is actually coming from the user or the binding.
Could you please suggest maybe and alternative way to do the binding to ensure we can distinguish between user update and binding udpates for the slider?
Thnak you!
UPDATE Sorry I forgot to mention why I am not binding directly both slider and textbox two ways like the below answers suggest. The update to the data context value is supposed to trigger a call to a backend server and retrieve data from a database. The problem is that when the user drags the slider it constantly fires updates. I go around the problem by only relying to the actual onValueChanged event to call the DoWhatever method. I hope that's a bit clearer. Sorry for omitting this...
I quickly put together the sample below for you to give it a try.
The xaml:
<Window x:Class="SliderIssue.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 HorizontalAlignment="Center"
VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Slider Name="slider" VerticalAlignment="Top"
ValueChanged="slider_ValueChanged"
Thumb.DragStarted="slider_DragStarted"
Thumb.DragCompleted="slider_DragCompleted"
Value="{Binding Count}"
Width="200"
Minimum="0"
Maximum="100"/>
<TextBox VerticalAlignment="Top"
HorizontalAlignment="Left"
Grid.Column="1"
Width="100"
Text="{Binding Count,Mode=TwoWay,UpdateSourceTrigger=LostFocus}"
Height="25"/>
</Grid>
The code behind:
using System.Windows;
namespace SliderIssue
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private bool _dragStarted;
public MainWindow()
{
InitializeComponent();
var item = new Item();
DataContext = item;
}
private void slider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
if (!_dragStarted)
{
var item = (Item)DataContext;
item.DoWhatever(e.NewValue);
}
}
private void slider_DragStarted(object sender, System.Windows.Controls.Primitives.DragStartedEventArgs e)
{
_dragStarted = true;
}
private void slider_DragCompleted(object sender, System.Windows.Controls.Primitives.DragCompletedEventArgs e)
{
_dragStarted = false;
var item = (Item) DataContext;
item.DoWhatever(slider.Value);
}
}
}
A simple data class:
using System.ComponentModel;
namespace SliderIssue
{
public class Item : INotifyPropertyChanged
{
private int _count = 50;
public int Count
{
get { return _count; }
set
{
if (_count != value)
{
_count = value;
DoWhatever(_count);
OnPropertyChanged("Count");
}
}
}
public void DoWhatever(double value)
{
//do something with value
//and blablabla
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
}
UPDATE
OK, now I see why you were trying to do it like that. I have a couple of suggestions that may help.
My first one is a bit more opinionated, but I offer it nonetheless. If the problem you are trying to solve is throttling requests to a back-end database, I would contend that your ViewModel need not concern itself with that. I would push that down a layer into an object that is making the call to the back-end based on the updated value passed down from the ViewModel.
You could create a poor-man's throttling attempt by recording DateTimeOffset.Now each time a call is made to the method to query the back-end DB. Compare that value to the last value recorded. If the TimeSpan between falls beneath your minimum threshold, update the value to which it was compared, and ignore the request.
You could do a similar thing with a timer and resetting the timer each time a request is made, but that is messier.
When the call returns from the back-end, this layer raises an event which the ViewModel handles and does whatever it needs to do with the data returned.
As another suggestion, I would also check out what the ReactiveExtensions give you. It takes a bit to kind of wrap your brain around how they work, but you could create an Observable from a stream of events, and then use the Throttle() method to return another Observable. You subscribe to that Observable and perform your call there. It would take more re-thinking the design and architecture of your software, but it is intriguing.
Paul Betts created an entire MVVM framework based around Rx called ReactiveUI. I first learned about throttling Observables in one of his blog posts here.
Good luck!
ORIGINAL POST
If I understand your problem correctly, it sounds like you would like both the Slider and the TextBox to reflect the same property of the DataContext (normally, a ViewModel). It looks like you are trying to duplicate what the binding mechanism of WPF gives you. I was able to get a quick prototype of this working. Here's the code I used.
For the view, I just created a new window with this as the content of the Window.
<StackPanel>
<Slider Value="{Binding TheValue}" Margin="16" />
<TextBox Text="{Binding TheValue}" Margin="16" />
</StackPanel>
Notice that both the Slider and the TextBox are bound to the same (cleverly-named) value of the DataContext. When the user enters a new value into the TextBox, the value will change, and the property change notification (in the ViewModel) will cause the slider to update its value automatically.
Here is the code for the ViewModel (i.e., the DataContext of the View).
class TextySlideyViewModel : ViewModelBase
{
private double _theValue;
public double TheValue
{
get { return _theValue; }
set
{
if(_theValue == value)
return;
_theValue = value;
OnPropertyChanged("TheValue");
}
}
}
My ViewModel is derived from a ViewModelBase class which implements the INotifyPropertyChanged interface. The OnPropertyChanged() method is defined in the base class which merely raises the event for the property whose name is passed as the parameter.
Lastly, I created the View and assigned a new instance of the ViewModel as its DataContext (I did this directly in the App's OnStartup() method for this example).
I hope this helps get you going in the right direction.
UPDATE:
Along the lines with Eric, but as a separate suggestion of operation.
Bind both controls to Count as two way as I suggested below.
Create a timer which fires off every second that checks two variables.
(Timer Check #1) Checks to see if a database request is ongoing (such as a Boolean flag). If it is true, it does nothing. If there is no operation (false), it goes to step 4.
(Timer Check #2) It checks to see if count is changed. If count has changed it sets the data request ongoing flag (as found/used in step 3) and initiates an async database call and exits.
(Database Action Call) Gets the database data and updates the VM accordingly. It sets the data request ongoing flag to false which allows the timer check to start a new request if count is changed.
That way you can manage the updates even if a user goes crazy with the slider.
I believe you may have over thought this. Remove all the events off of the slider and the textbox. If the first value (set programmatically) should not call your DoWhatever method, then put in a check in that code to skip the first initialization....
I recommend that you make the slider bind to Count as a TwoWay mode and have the Count Property do the other process you need (as shown on your entity class). No need to check for clicks or any other event. If the user changes the value in the textbox it changes the slider and visa versa.
<Slider Name="slider"
VerticalAlignment="Top"
Value="{Binding Count, Mode=TwoWay}"
Width="200"
Minimum="0"
Maximum="100" />
<TextBox VerticalAlignment="Top"
HorizontalAlignment="Left"
Grid.Column="1"
Width="100"
Text="{Binding Count,Mode=TwoWay,UpdateSourceTrigger=LostFocus}"
Height="25" />
I have a DataGrid. One of the columns is a template with a CheckBox in it. When the Checked or Unchecked events trigger, (it happens with both) the CheckBox's DataContext is sometimes null, which causes my code to error. It seems to be null most often if the mouse is moving while you press and release the button quickly (it's intermittent).
I listened for changes to the DataContext of the CheckBox by making views:ListenCheckBox (extends CheckBox) and attaching a binding, and it's never set to null, but it is set from null to a Task at times I wouldn't expect, i.e. after the DataGrid has been totally generated and you're checking/unchecking boxes. Immediately after the [un]checked event runs with a null DataContext, I get the notification that shows the DataContext changed from null to a Task, so it appears that when I get a null DataContext, it's because it hadn't actually set the DataContext by the time it ran the Checked/Unchecked event.
Also, I added Tag="{Binding}" to the CheckBox for debugging. The Tag is not null (i.e. it has the proper object) more often than the DataContext, but still not all the time.
Here are the relevant bits of the XAML code:
<navigation:Page.Resources>
<sdk:DataGridTemplateColumn x:Key="DeleteOrPrintSelect" Header="Delete Or Print Notes Selection">
<sdk:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<views:ListenCheckBox IsChecked="{Binding DeleteOrPrintNotesSelection, Mode=TwoWay}" Checked="DeletePrintNotesCheckBox_Changed" Unchecked="DeletePrintNotesCheckBox_Changed" HorizontalAlignment="Center" VerticalAlignment="Center" Tag="{Binding}" />
</DataTemplate>
</sdk:DataGridTemplateColumn.CellTemplate>
</sdk:DataGridTemplateColumn>
</navigation:Page.Resources>
<sdk:DataGrid x:Name="dataGrid1" Grid.Column="1" Grid.Row="2" AutoGeneratingColumn="dataGrid1_AutoGeneratingColumn">
<sdk:DataGrid.RowGroupHeaderStyles>
[removed]
</sdk:DataGrid.RowGroupHeaderStyles>
</sdk:DataGrid>
And the relevant code behind:
// Create a collection to store task data.
ObservableCollection<Task> taskList = new ObservableCollection<Task>();
[code adding Tasks to taskList removed]
PagedCollectionView panelListView = new PagedCollectionView(taskList);
this.dataGrid1.ItemsSource = panelListView;
}
private void dataGrid1_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
if (e.PropertyName == "DeleteOrPrintNotesSelection")
{
e.Column = Resources["DeleteOrPrintSelect"] as DataGridTemplateColumn;
}
else
{
e.Column.IsReadOnly = true;
}
}
private void DeletePrintNotesCheckBox_Changed(object sender, RoutedEventArgs e)
{
try
{
var cb = sender as CheckBox;
var t = cb.DataContext as Task;
t.DeleteOrPrintNotesSelection = cb.IsChecked == true;
PagedCollectionView pcv = this.dataGrid1.ItemsSource as PagedCollectionView;
ObservableCollection<Task> taskList = pcv.SourceCollection as ObservableCollection<Task>;
bool anySelected = taskList.Any(x => x.DeleteOrPrintNotesSelection);
this.btnPrint.IsEnabled = anySelected;
this.btnDelete.IsEnabled = anySelected;
}
catch (Exception ex)
{
ErrorMessageBox.Show("recheck", ex, this);
}
}
Any ideas? Thanks in advance.
I found that the problem happened when you double click on the cell and it moved it to the cell editing template. In my case, I didn't have a cell editing template defined, so it used the same cell template, but instead of not changing anything, it apparently decided to make a new check box. I set the column's IsReadOnly property to true, and it fixed it. An alternate solution:
DataContext="{Binding}" (in XAML, or the code equivalent:)
cb.SetBinding(FrameworkElement.DataContextProperty, new Binding());
I'm not sure why this one fixes it, since I thought the default DataContext is {Binding}. Perhaps it's a Silverlight bug, and it gets set in a different order if you define it explicitly instead of leaving it the default.
This is a running question that I have updated to hopefully be a little more clear.
In short what I am trying to accomplish is pass a property from a listbox selected item to the viewmodel so that this property can be used within a new query. In the code below the Listbox inherits databinding from the parent object. The listbox contains data templates (user controls) used to render out detailed results.
The issue I am having is that within the user control I have an expander which when clicked calls a command from the ViewModel. From what I can see the Listbox object is loosing it's data context so in order for the command to be called when the expander is expanded I have to explicitly set the datacontext of the expander. Doing this seems to instantiate a new view model which resets my bound property (SelectedItemsID) to null.
Is there a way to pass the selected item from the view to the viewmodel and prevent the value from being reset to null when a button calls a command from within the templated listbox item?
I realize that both Prism and MVVMLite have workarounds for this but I am not familiar with either framework so I don't know the level of complexity in cutting either of these into my project.
Can this be accomplished outside of Prism or MVVMLite?
original post follows:
Within my project I have a listbox usercontrol which contains a custom data template.
<ListBox x:Name="ResultListBox"
HorizontalAlignment="Stretch"
Background="{x:Null}"
BorderThickness="0"
HorizontalContentAlignment="Stretch"
ItemsSource="{Binding SearchResults[0].Results,
Mode=TwoWay}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
SelectionChanged="ResultListBox_SelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate>
<dts:TypeTemplateSelector Content="{Binding}" HorizontalContentAlignment="Stretch">
<!-- CFS Template -->
<dts:TypeTemplateSelector.CFSTemplate>
<DataTemplate>
<qr:srchCFS />
</DataTemplate>
</dts:TypeTemplateSelector.CFSTemplate>
<!-- Person Template -->
<dts:TypeTemplateSelector.PersonTemplate>
<DataTemplate>
<qr:srchPerson />
</DataTemplate>
</dts:TypeTemplateSelector.PersonTemplate>
<!-- removed for brevity -->
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
SelectionChanged calls the following method from the code behind
private void ResultListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (((ListBox)sender).SelectedItem != null)
_ViewModel.SelectedItemID = (((ListBox)sender).SelectedItem as QueryResult).ID.ToString();
this.NotifyPropertyChanged(_ViewModel.SelectedItemID);//binds to VM
}
Within the ViewModel I have the following property
public string SelectedItemID
{
get
{
return this._SelectedItemID;
}
set
{
if (this._SelectedItemID == value)
return;
this._SelectedItemID = value;
}
}
the listbox template contains a custom layout with an expander control. The expander control is used to display more details related to the selected item. These details (collection) are created by making a new call to my proxy. To do this with an expander control I used the Expressions InvokeCommandAction
<toolkit:Expander Height="auto"
Margin="0,0,-2,0"
Foreground="#FFFFC21C"
Header="View Details"
IsExpanded="False"
DataContext="{Binding Source={StaticResource SearchViewModelDataSource}}"
Style="{StaticResource DetailExpander}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Expanded">
<i:InvokeCommandAction Command="{Binding GetCfsResultCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
Within the ViewModel the delegate command GetCFSResultCommandExecute which is called is fairly straight forward
private void GetCfsResultCommandExecute(object parameter)
{
long IdResult;
if (long.TryParse(SelectedItemID, out IdResult))
{
this.CallForServiceResults = this._DataModel.GetCFSResults(IdResult);}
The issue I am experiencing is when selecting a listbox Item the selectionchanged event fires and the property SelectedItemID is updated with the correct id from the selected item. When I click on the expander the Command is fired but the property SelectedItemID is set to null. I have traced this with Silverlight-Spy and the events are consistent with what you would expect when the expander is clicked the listbox item loses focus, the expander (toggle) gets focus and there is a LeftMouseDownEvent but I cannot see anything happening that explains why the property is being set to null. I added the same code used in the selection changed event to a LostFocus event on the listboxt item and still received the same result.
I'd appreciate any help with understanding why the public property SelectedItemID is being set to null when the expander button which is part of the listbox control is being set to null. And of course I would REALLY appreciate any help in learning how prevent the property from being set to null and retaining the bound ID.
Update
I have attempted to remove the datacontext reference from the Expander as this was suggested to be the issue. From what I have since this is a data template item it "steps" out of the visual tree and looses reference to the datacontext of the control which is inherited from the parent object. If I attempt to set the datacontext in code for the control all bindings to properties are lost.
My next attempt was to set the datacontext for the expander control within the constructor as
private SearchViewModel _ViewModel;
public srchCFS()
{
InitializeComponent();
this.cfsExpander.DataContext = this._ViewModel;
}
This approach does not seem to work as InvokeCommandAction is never fired. This command only seems to trigger if data context is set on the expander.
thanks in advance
With this line you create a new SearchViewModelDataSource using its default constructor.
DataContext="{Binding Source={StaticResource SearchViewModelDataSource}}"
I guess this is why you find null because this is the default value for reference type.
You can resolve the issue by setting DataContext to the same instance used to the main controll (you can do it by code after all components are initialized).
Hope this help!
Edit
I don't think that binding may be lost after setting datacontext from code. I do it every time I need to share something between two or more model.
In relation to the code you've written :
private SearchViewModel _ViewModel;
public srchCFS()
{
InitializeComponent();
this.cfsExpander.DataContext = this._ViewModel;
}
Instead of using this.cfsExpander you can try to use the FindName method. Maybe this will return you the correct instance.
object item = this.FindName("expander_name");
if ((item!=null)&&(item is Expander))
{
Expander exp = item as Expander;
exp.DataContext = this._ViewModel;
}
Try if its work for you.
Of course, this._ViewModel has to expose a property of type ICommand named GetCfsResultCommand but I think this has been already done.
While this was a hacky approach I found an intermediate solution to get the listbox item value to the view model. I ended up using the selection changed event and passing the value directly to a public property wihtin my view model. Not the best approach but it resolved the issue short term
private void ResultListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (((ListBox)sender).SelectedItem != null)
_ViewModel.SelectedItemID = (((ListBox)sender).SelectedItem as QueryResult).ID.ToString();
MySelectedValue = (((ListBox)sender).SelectedItem as QueryResult).ID.ToString();
this.NotifyPropertyChanged(_ViewModel.SelectedItemID);
}
For this to fire I did have to also setup a property changed handler within the view to push the change to the VM. You can disregard the MySelectedValue line as it is secondary code I have in place for testing.
For those intereted the generic property changed handler
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
Good morning,
Apologies for the mass of text im about to provide but...
I have a WPF ListView with its ItemsSource bound to an ObservableCollection in it's respective ViewModel. When the window loads, the observable collection is populated from a web service by means of a Command. However, as the program is running, this collection is periodically updated by a BackgroundWorker thread in order to add new items to the ObservableCollection.
This mechanism works fine. The ListView is updated without issue on both the UI thread and the background thread. However, when an item in the ListView is double clicked, a new window is opened to display details of the Ticket object contained within the aforementioned ObservableCollection.
I have a private method which fires each time the ObservableCollection's set method is called which serves to find the Ticket item from within the collection which has been opened in the new window and update its properties according to the items in the newly updated ObservableCollection. Before doing this update, I check to ensure the ObservableCollection.Count is greater than 1, there is no point doing an update if there is nothing to update from!
My issue is that the ObservableCollection.Count property ALWAYS equates to 0. But I know this not to be true as the ListView is still updating its items with new Ticket objects added to this collection, if the count of this collection really was 0, then this would be reflected by the ListView also having no items in it as it is bound to this collection.
So what is going on here ? Im wondering maybe because the BackgroundWorker is calling;
myCollection = new ObservableCollection();
on a different thread to the UI that when I check the count on the UI thread, the wrong collection object is actually tested for 'Count'. But this still doesn't explain why the ListView reflects the contents of the ObservableCollection without issue.
Again, apologies for the wall-o-text but I wanted to explain this issue fully.
Thank you for your time and any input you may give.
EDIT FOR MORE DETAIL
The list view section of user control
<ListView x:Name="lvTicketSummaries" ItemsSource="{Binding Path=TicketSummaries}" Grid.Row="1" Width="Auto" Height="Auto" SizeChanged="lvTicketSummaries_SizeChanged" SelectionMode="Single" Foreground="Black" Background="#3BFFFFFF" ItemContainerStyle="{DynamicResource ListViewItem}">
<ListView.View>
<GridView AllowsColumnReorder="True">
<GridViewColumn Header="ID"
DisplayMemberBinding="{Binding ID}"
Width="25"/>
<GridViewColumn Header="Status"
DisplayMemberBinding="{Binding Status}"
Width="25"/>
<GridViewColumn Header="Subject"
DisplayMemberBinding="{Binding Subject}"
Width="25"/>
<GridViewColumn Header="Requester"
DisplayMemberBinding="{Binding Owner.Name}"
Width="25"/>
</GridView>
</ListView.View>
</ListView>
The view model of the above user control
Here you see the TicketSummaries collection which the list view is bound to as well as the refreshOpenTicket() method used to update the Ticket property in a child view model which the new instance of itself in the newly refreshed collection.
public class MainWindowViewModel : ViewModelBase
{
private DispatcherTimer timer;
private BackgroundWorker worker_TicketLoader;
private ObservableCollection<Ticket> ticketSummaries;
public ObservableCollection<Ticket> TicketSummaries
{
get { return ticketSummaries; }
set
{
ticketSummaries = value;
this.RaisePropertyChanged(p => p.TicketSummaries);
refreshOpenTicket();
}
}
private void refreshOpenTicket()
{
// Check there are actually some tickets to refresh
if (TicketSummaries.Count < 1)
return;
// Check we have created the view model
if (TicketDetailsViewModel != null)
{
// Check the ticket loaded correctly
if (TicketDetailsViewModel.Ticket != null)
{
// Find a ticket in the collection with the same id
Ticket openTicket = TicketSummaries.Where(
ticket => ticket.ID == TicketDetailsViewModel.Ticket.ID
).First();
// Make sure we are not going to overrite with a null reference
if (openTicket != null)
TicketDetailsViewModel.Ticket = openTicket;
}
}
}
This collection is updated from various sources via the following command
private void Execute_GetAgentsTickets(object agent)
{
TicketSummaries = new ObservableCollection<Ticket>();
var agentsTickets = ticketService.GetAgentsTickets((Agent)agent);
agentsTickets.ForEach(
ticket => TicketSummaries.Add(ticket)
);
AppSettings.LoggedAgent = (Agent)agent;
RequeryCommands();
}
But occasionally this collection will be modified off-thread by the background worker
void worker_TicketLoader_DoWork(object sender, DoWorkEventArgs e)
{
State = "Loading Tickets";
IsLoadingTickets = true;
var agentsTickets = ticketService.GetAgentsTickets(AppSettings.LoggedAgent);
TicketSummaries = new ObservableCollection<Ticket>();
foreach (Ticket ticket in agentsTickets)
{
TicketSummaries.AddOnUIThread<Ticket>(ticket);
}
refreshOpenTicket();
lastRefresh = DateTime.Now;
}
Just in case it makes a difference, the TicketSummaries.AddOnUIThread(ticket); is a solution I found on StackOverflow to trying to add items to a collection which is a binding source to UI controls off-thread and is as;
public static void AddOnUIThread<T>(this ICollection<T> collection, T item)
{
Action<T> addMethod = collection.Add;
Application.Current.Dispatcher.BeginInvoke(addMethod, item);
}
I hope this helps shed some more light on the situation.
Thanks again for your time.
You haven't provided enough information to diagnose, but I'm guessing the ListView is bound to a property and when you replace the ObservableCollection, you're not updating that property. Hence, the ListView is still attached to the original collection whilst your VM code is working with a new one.
Why replace the OC at all? Why not just update it as items come through from the middle tier? If you really must replace it, be sure to go via a property and raise change notification for that property.