I have a ListBox bound to an ObservableCollection with an ItemTemplate that contains another ListBox. First of all, I tried to get the last selected item of all the listboxes (either the parent and the inner ones) from my MainWindowViewModel this way:
public object SelectedItem
{
get { return this.selectedItem; }
set
{
this.selectedItem = value;
base.NotifyPropertyChanged("SelectedItem");
}
}
So, for example, in the DataTemplate of the items of the parent ListBox I've got this:
<ListBox ItemsSource="{Binding Tails}"
SelectedItem="{Binding Path=DataContext.SelectedItem, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"/>
The problem now, is that when I select an item from the parent ListBox and then an item from a child listbox, I get this:
http://i40.tinypic.com/j7bvig.jpg
As you can see, two items are selected at the same time. How can I solve that?
Thanks in advance.
I have already solved this issue by registering a ClassHandler for the SelectedEvent of the ListBox control.
I just added this in the constructor of my MainWindow class:
EventManager.RegisterClassHandler(typeof(ListBox),
ListBox.SelectedEvent,
new RoutedEventHandler(this.ListBox_OnSelected));
That way, my ListBox_OnSelected event handler will be called whenever a listbox is called, and before the event handlers of the control itself are called.
In the MainWindowViewModel I have a property called SelectedListBox that keeps track of which one is selected:
public System.Windows.Controls.ListBox SelectedListBox
{
get { return this.selectedListBox; }
set
{
if (this.selectedListBox != null)
{
this.selectedListBox.UnselectAll();
}
this.selectedListBox = value;
}
}
Why not using a simple SelectionChanged event handler? Because in the above code, every time you unselect a listbox it raises again the same event, getting an infinite loop of events that fortunately WPF is able to stop.
Related
I have a user control containing an expander. The content of the explander is a ListBox bound to an object, and a DataTemplate displays it correctly. The problem is this: the user can select a Listbox item, and the SelectionChanged handler changed the DataContext of the ListBox to the selected object.
Like this:
<ListBox
Name="RelativesLB" ItemsSource="{Binding Relatives}",
ItemsTemplate ="{...}",
Selectionchanged="Relatives_OnSelectionChanged" />
And:
Relatives_OnSelectionChanged(object sender, ...EventArgs e)
{
var who = (sender as ListBox).SelectedItem as Person;
if (who == null)
return;
People.DataContext = who;
Here is the problem:
The SelectionChanged event fires.
The DataContext is changed, and the ListBox repopulates.
The SelectionChanged event fires with SelectedItem = null. Here, my code does not change the DataContext; it just returns.
the SelectionChanged event fires again with SelectedItem = <whatever is first>. Here, my code changes the DataContext again to that item I don't want this bit. Actually, I want to stop after 2.
the Datacontext is changed to <whatever is first>
...
and so on, until we get an empty Person.Relatives, then we stop.
What I want is the stop after the first DataContext change. You select a person from the Relatives collection, and get the view for that person.
How can I stop the subsequent SelectionChanged events firing?
I guess, in your on Relatives_OnSelectionChanged you need to set
e.Handled = True;
i have a container such as a listbox, combobox etc that its ItemsSource property is bound to an observable collection in my view model.
When i'm trying to add/remove items from the collection via some method in my VM it won't reflect in the UI,
The only way the UI would actually refresh is if i assign the collection a new value (i.e another collection with the relevant data) which forces him to re-bind the whole collection.
maybe i'm missing/don't understand something about the collection binding issue, either way if someone has a solution/good explanation/both it would be great.
here is a sample from my View(in this case its a listbox)
<ListBox
Grid.Row="9"
Grid.Column="1"
Grid.ColumnSpan="3"
Width="200"
Height="200"
ItemsSource="{Binding PreSavedRecordingScheduleList,UpdateSourceTrigger=PropertyChanged}"
SelectedItem="{Binding SelectedPreSavedRecordingSchedule,UpdateSourceTrigger=PropertyChanged}"
DisplayMemberPath="Display"/>
and here is my ViewModel:
private ObservableCollection<ScheduledRecordingObject> m_PreSavedRecordingScheduleList;
PreSavedRecordingScheduleList = new ObservableCollection<ScheduledRecordingObject>();
public ObservableCollection<ScheduledRecordingObject> PreSavedRecordingScheduleList
{
get
{
return m_PreSavedRecordingScheduleList;
}
set
{
m_PreSavedRecordingScheduleList = value;
OnPropertyChanged("PreSavedRecordingScheduleList");
}
}
ScheduledRecordingObject also implements INotifyPropertyChanged.
viewmodel
public ObservableCollection<yourType> MyItemsSource {get;set}
initialize once in contructor and use clear, add and remove to alter it
view
<ListBox ItemsSource="{Binding MyItemsSource}"/>
just be sure that the right DataContext is set.
thats how it should look in your code
EDIT:
some hints to your posted code:
//remove the UpdateSourceTrigger=PropertyChanged - makes no sense the Mode is OneWay anyway :)
ItemsSource="{Binding PreSavedRecordingScheduleList}"
//the following line should just called once and at best in ctor
//but the binding will of course work too when you assign a new collection
PreSavedRecordingScheduleList = new ObservableCollection<ScheduledRecordingObject>();
all in all your code looks good, and if the viewmodel is the Datacontext of your Listbox then it should work. let me know what Snoop is showing :)
Remove the OnPropertyChanged("PreSavedRecordingScheduleList"); from the ObservableCollection. Actually you don't need a backing field. Attach the CollectionChanged event on the ObservableCollection, something like this
1- Inside the ViewModel constructor attach the event CollectionChanged
PreSavedRecordingScheduleList = new ObservableCollection<ScheduledRecordingObject>();
PreSavedRecordingScheduleList.CollectionChanged += PreSavedRecordingScheduleList_CollectionChanged;
2- Inject the OnPropertyChanged("PreSavedRecordingScheduleList") in the event handler
void PreSavedRecordingScheduleList_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
OnPropertyChanged("PreSavedRecordingScheduleList");
}
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));
}
}
I have two TreeView´s in a TabControl, databound to a xmlDataProvider. If i add nodes to my Xml and save it:
xmlDataProvider.Document.Save(fullPathToXml);
xmlDataProvider.Refresh();
Only the TreeView that is not in the open Tab refresh. Both TreeView´s look like this:
<TreeView Name="DIFFERENT_NAMES" ItemsSource="{Binding Source={StaticResource dataxml}, XPath=./*}"/>
If your ItemsSource is binded to a property implementing OnPropertyChanged, you can add the attribute "UpdateSourceTrigger=PropertyChanges" to your binding in the XAML.
Hence, the control will update itself each time OnPropertyChanged is called
EDIT
I assume your ViewModel already implements OnPropertyChanged
Therefore, all you have to do is, when you declare your property:
private XmlDataProvider _xmlDataProvider;
public XmlDataProvider XmlDataProvider
{
get { return xmlDataProvider; }
set
{
xmlDataProvider = value;
OnPropertyChanged("XmlDataProvider");
}
}
Initialize the XmlDataProvider in your constructor, and then, each time the object is modified, it will call the method OnPropertyChanged, on the property you specify (here "XmlDataProvider"), and each time OnPropertyChanged is called, your view bound to this object will refresh automatically :)
I have a TabControl whose items are bound to an ObservableCollection:
<TabControl ItemsSource="{Binding MyObservableCollection}" />
The tabs are added and removed as expected as items are added and removed from the collection. However, the SelectedItem reverts to -1 (meaning there is no selected tab) whenever the collection is empty. Then, when an item is added, the SelectedItem stays at -1 and the new tab is not selected.
How do I make the TabControl select the new tab whenever an item is added to the empty collection?
There might be an easier way, but you could hook the collection changed event on the ObservableCollection in your VM and set the SelectedItem property to the new item (assuming you have the selected item bound to a property on the VM).
What you can do is to subscribe for TabControl.ItemContainerGenerator.StatusChanged event and if the status is ContainersGenerated and the SelectedIndex of TabControl is -1, then to make the SelectedIndex of TabControl 0;
// peopleCollection is an ObservableCollection<Person>
People peopleCollection = new People();
public Window1()
{
InitializeComponent();
// MyTabControl is an instance of TabControl
MyTabControl.ItemsSource = peopleCollection;
MyTabControl.ItemContainerGenerator.StatusChanged += new EventHandler(ItemContainerGenerator_StatusChanged);
}
void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
if((sender as ItemContainerGenerator).Status == System.Windows.Controls.Primitives.GeneratorStatus.ContainersGenerated && MyTabControl.SelectedIndex == -1)
{
MyTabControl.SelectedIndex = 0;
}
}
There are 3-rd party solutions that has this functionality out of the box. Telerik's RadTabControl selects the first item whenever the collection changes its state from empty to "containing single item".
Try the demo here: http://demos.telerik.com/silverlight/#TabControl/AddingAndRemovingTabs
Note: It is a SL demo, but it works the same in WPF.
If you are looking for a pure MVVM implementation then, add a Index property to the ViewModel and on the CollectionChanged you can set Index=0 if there is no items inside. And in the XAML you can bind that Index as below
<TabControl ItemsSource="{Binding MyObservableCollection}" SelectedIndex="{Binding Index}" />
you're best bet is to probably overwrite the "OnTabAdded" functionality to check if a new one (first one) is added and then setting the SelectedItemIndex to 0;
since you are using ObservableCollection, you know when your collection changes, so I'd subscribe to the changed event form the collection and check the number of items in it.
I had the same problem and managed to fix it by binding the selected item to the first item in the dynamic list.
<TabControl ItemsSource="{Binding MyObservableCollection}" SelectedItem="{Binding MyObservableCollection.First}" />
Worked for me :)
<TabControl ItemsSource="{Binding MyObservableCollection}" SelectedItem="{Binding MyObservableCollection[0]}" />