How to make sure my WPF TabControl always has a selected tab when it contains at least one tab? - wpf

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]}" />

Related

Binding Observable collection list to dynamically created ComboBox in Silverlight

I have a requirement where I need to bind a list of observable collections to the item source of dynamically created ComboBoxes.
The problem is we bind through xaml conventionally in ItemsSource property but now the control rows in the grid are being added dynamically so each ComboBox in a row reference to the same collection whereas I need to bind it to a separate collection in observable collection list each time a row in the grid is added.
This what I have tried so far, any guidance would be appreciated. Thanks.
public virtual List<ObservableCollection<ComboBoxEntity>> ListRewardRule { get; set; }
Xaml :
<itimControls:ComboBox Name="cboReward"
IsMandatory="True"
itimComponents:ComponentManager.ComponentId="TXT_GROUP_RULE"
MaxWidth="400"
MinWidth="150"
ItemsSource="{Binding ListRewardRule, ElementName=RDefinitionScreen}"
DisplayMemberPath="Name"
SelectedValuePath="Code"
Loaded="cboReward_Loaded"
SelectedValue="{Binding RewardRuleId, Mode=TwoWay}"
SelectionChanged="cboReward_SelectionChanged">
</itimControls:ComboBox>
.CS :
private void cboReward_Loaded(object sender, RoutedEventArgs e)
{
Itim.Framework.Silverlight.UI.Controls.ComboBox cboReward = ((Itim.Framework.Silverlight.UI.Controls.ComboBox)sender);
int row = (int)cboReward.GetValue(Grid.RowProperty);
if (Model.ListRewardRule.Count > 0)
{
var rewardGroups = Model.RewardGroupAndTier.RewardGroups;
if(rewardGroups.Count > 1)
{
cboReward.ItemsSource = Model.ListRewardRule[row];
}
}
}
The way I understand this is, you have a “grid” (I suppose you mean a DataGrid) and each row in the grid has, among other things, a set of combo boxes.
What I’d do is bind that grid to an ObservableCollection of a custom class, call it CustomRowClass. CustomRowClass should have an ObservableCollection that will be bound to the combo boxes.
The magic is to define a DataTemplate for CustomRowClass. Once you set up the XAML this way, all you need to do is create an instance of CustomRowClass, and then add it to the ItemsSource of the grid.

WPF MVVM ListBox/ComboBox ItemsSource won't update UI from ViewModel

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");
}

Combo-box loses selection after collection changes

I have a WPF combo box bound to an obvserable collection (OC):
<ComboBox Name="cbCombination" ItemsSource="{Binding Combinations}"
SelectedIndex="0" />
Elsewhere, in the object set as data context:
public ObservableCollection<Combination> Combinations { get; set; }
Combination overrides its ToString and everything is peachy: The combo-box's drop-down displays all Combination items in the Combinations OC. The combo-box's selection box displays the value of the first Combination.
Now, the data-context object has to change the values in its Combinations OC:
var combinationsList = CombinationsManager.CombinationsFor(someParam);
this.Combinations.Clear();
foreach (var combination in combinationsList)
this.Combinations.Add(combination);
NotifyPropertyChanged(#"Combinations");
This causes the combo-box's selection box shows an empty string. (The drop-down is closed. However, when I make it drop down, it does show the correct new Combinations, so it is bound to the updated collection).
I tried to capture both SourceUpdated and (in my dispair) TargetUpdated events (thining of setting the SelectedIndex there), but my event handlers didn't get called!
So my question is: How do I make a WPF ComboBox refresh the value of its selection-box when the observable collection it is bound-to changes?
Update:
I've totally forgotten to mention, and I don't know whether it's important, but the combo-box is within a UserControl. The UserControl's XAML looks like this:
<UserControl x:Class="...CombinationsToolBar"
.... mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<ToolBarTray Name="toolBarTray1" >
<ToolBar Name="toolBar1">
<ComboBox Name="cbCombination"
ItemsSource="{Binding Path=Combinations, NotifyOnSourceUpdated=True,
IsAsync=True}"
SelectedIndex="0" IsEditable="False"
SelectionChanged="CbCombinationSelectionChanged"
SourceUpdated="CbCombinationSourceUpdated"
TargetUpdated="CbCombinationTargetUpdated"></ComboBox>
</ToolBar>
</ToolBarTray>
In the UserControl's code I have breakpoints on CbCombinationSelectionChanged, CbCombinationSourceUpdated and CbCombinationTargetUpdated.
The CbCombinationSelectionChanged fires once when the form containing the user control is first loaded. It is indeed called a second time when the Combinations collection is cleared, as #asktomsk said.
The source updated and target updated are not triggered - CbCombinationSourceUpdated and CbCombinationTargetUpdated are not called.
As the combo box is inside a usercontrol and the Combinations collection is within the view model, and as the view model doesn't have access to the combo box, I have no opportunity to set the selected index of the combo unless the events fire.
:(
The problem is in your this.Combinations.Clear();
When you do it, it sets SelectedItem = null and SelectedIndex = -1
So you should set selection again. If you have an access to your ComboBox then just write this cbCombination.SelectedIndex = 0; after filling Combinations list.
Of course you can bind SelectedItem/SelectedIndex to some property in code behind too.
BTW
Also it is not required to call NotifyPropertyChanged(#"Combinations"); after filling the collection because Combinations already implements INotifyPropertyChanged.
Update
To detect that your ObservableCollection was changed, subscribe to CollectionChanged event in your UserControl code behind. Make sure that you subscribed before collection changed!
Combinations.CollectionChanged += (s, e) =>
{
if (cbCombination.Items.Count > 0)
cbCombination.SelectedIndex = 0;
};
Another suggestion
Approach above is works when you do not need a smarter logic than just select zero index in combobox.
But often you will need a more complex logic to select some item. In this case you may add new property to your model:
public Combination SelectedCombination
{
get{ return _selectedCombination; }
set
{
_selectedCombination = value;
NotifyPropertyChanged("SelectedCombination");
}
}
and bind this property to your combobox:
<ComboBox Name="cbCombination" ItemsSource="{Binding Combinations}"
SelectedIndex="0" SelectedItem={Bindings SelectedCombination} />
In this case you can select any item when filling the Combinations collection and it will be automatically selected in combobox:
var combinationsList = CombinationsManager.CombinationsFor(someParam);
this.Combinations.Clear();
foreach (var combination in combinationsList)
this.Combinations.Add(combination);
if (Combinations.Count > 0)
SelectedCombination = Combinations[0];

How to pass the selectedItem of a listbox to the View Model

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));
}
}

How to stop automatic refresh of a WPF ListBox Databinded on a EntityFramework object

I have a Xaml Page with a Databinded ListBox and a detail grid to create or update selected element.
My Page.DataContext is binded on a ADO.NET Entity Data Model table ("Univers").
private void Page_Loaded(object sender, RoutedEventArgs e)
{
SEPDC = new Models.SEP();
universViewSource = new CollectionViewSource();
universViewSource.Source = SEPDC.Univers.Execute(System.Data.Objects.MergeOption.AppendOnly);
DataContext = universViewSource;
}
The Xaml code of the ListBox :
<ListBox DisplayMemberPath="Nom" ItemsSource="{Binding}" Name="universListBox" SelectedValuePath="IdUnivers"/>
When i select an element in the ListBox, the grid detail automatically display the information of the selected element
Here the "Nom" TextBox witch use TwoWay databinding :
<TextBox Name="nomTextBox" Text="{Binding Path=Nom, Mode=TwoWay}" />
When i modify the TextBox "Nom", the ListBox is automatically updated. Great ... But i haven't call the SaveChanges method of my SEPDC DataContext object ...
How can i stop the automatic refresh of my ListBox until i explicit call the SaveChanges method and if possible, without use the Binding UpdateSourceTrigger=Explicit option ?
Regards.
You can use two separate entity data context (SEPDC) objects. Your ListBox is bound to one and your detail grid to the other. When the SelectedValue changes in the ListBox, find the same entity in the detail grid's entity data context and set it. After saving changes from the detail grid's entity data context, refresh the one for the ListBox.
I use this technique but i have to recreate the ListBox SEPDC each time i refresh the ListBox.
List<Models.Univers> list;
using (Models.SEP dc = new Models.SEP())
list = dc.Univers.Execute(System.Data.Objects.MergeOption.AppendOnly).ToList();
universListBox.DataContext = list;
The Refresh method doesn't work.
Regards

Resources