WPF TabControl not firing Loaded event of unfocused children - c#

I have a TabControl with a binding for the ItemsSource property. It's bound to a ObservableCollection. When I add an item to the collection, the tabs get created correctly, but only the first tab gets its Grid_Loaded event fired. I'm guessing this is because it's focused. I need to initialize stuff when a new tab opens, it contains a control that needs to be referenced.
<TabControl x:Name="tabSessions"
ItemsSource="{Binding Sessions}"
SelectedIndex="0"
BorderThickness="0"
Padding="0,0,0,0">
<TabControl.ItemTemplate>
<DataTemplate DataType="{x:Type vm:MyViewModel}">
<TextBlock Margin="4,4,16,4" Text="{Binding Name}"></TextBlock>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<Grid Loaded="Grid_Loaded">
<!-- View here -->
</Grid>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
Is there an event that I could use to initialize this? I also need the DataContext to be set, and the view to be initialized.c#

The DataTemplate is reused by the tabs, which is why you're probably only seeing one Loaded event. It's only loaded the first time and then reused when you switch tabs. Only the binded content is changed.
Instead of listening to Loaded on the Grid, you could rather listen to DataContextChanged which will tell you every time the DataContext is set to a new object.
The DataContext will change when you switch tabs. The current tab will be set as the DataContext of the DataTemplate (and by extensions the Grid).
When using a DataTemplate you assume that the view is going to be the same, but the content is going to differ. If this is not the case and the views will differ based on the content, you'll probably want to look into using a DataTemplateSelector. This will let you define several DataTemplates and select one of them to use based on the current DataContext. You can read up on DataTemplateSelector in the Microsoft Docs

Related

How to dynamically generate grids from a variety of templates

I am relatively new to WPF and I'm trying to create a system where you can click a button to add a new tab which generates a corresponding grid to display content. For example, when you open a new tab, the first page would be "Home page", you then have options within this new tab to navigate to other content such as "Options". You can then add another tab and repeat the process etc. IT would be like Google Chrome how each tab represents another browser experience.
So far I have my tabs working, however I am stuck with how I am supposed to generate grids for each tab. Does anyone have any suggestions on how I could do this?
Create an abstract TabData base class with a Header string property.
Create a class that derives from TabData for each type of tab that you want (e.g. HomeData, OptionsData, etc.)
Create a view model with an Items property of type IEnumerable<TabData>.
Bind the ItemsSource property of the TabControl to Items and define an implicit DataTemplate for each concrete TabData type:
<TabControl ItemsSource="{Binding Items}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Header}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.Resources>
<DataTemplate DataType="{x:Type local:HomeData}">
...
</DataTemplate>
<DataTemplate DataType="{x:Type local:OptionsData}">
...
</DataTemplate>
</TabControl.Resources>
</TabControl>
The DataTemplate defines the appearance of a tab and may contain a DataGrid or any other element.

React on (before) datatemplate change - Or prevent listbox item change at this moment

In my application I'm using a simple view selector using datatemplates, I basically have a content control and datatemplates like
<UserControl.Resources>
<DataTemplate DataType="{x:Type viewmodels:OverviewViewModel}">
<local:OverviewView></local:OverviewView>
</DataTemplate>
<DataTemplate DataType="{x:Type viewmodels:DetailViewModel}">
<local:DetailView></local:DetailView>
</DataTemplate>
</UserControl.Resources>
<Grid>
<ContentControl Content="{Binding CurrentView}" Grid.Column="0"/>
</Grid>
Simple, efficient and easily extendable. (Key point is that these usercontrol views can be recursive). The main view also holds a few button with commands behind them to go to "another view".
The problem is, the DetailView contains a ListBox (ListView to be exact), at which "selected items" is of actual importance for the application state.
Now I notice that if I switch from the DetailView to the OverviewView the SelectionChanged event fires - actually unselecting all elements. Looking through the callstack this is due to the fact that the actual elements inside the ListView are updated, the listview is being cleared when the datatemplate changes.
Now since this actually changes the application state this is bad. (The user didn't actually click to unselect the elements, the selection is one of the main reasons to actually open the detail view).
I've tried catching the Unloaded event from the ListBox: this event is indeed fired. But also AFTER the selectionchanged event is fired, so it doesn't really help here.
So can I hook up to a "this UserControl will be unloaded momentarily" event? The bigger UserControl (and the ViewModel for that) should be considered a black box from the smaller DetailView/DetailViewModel.

Forcing a ListBox to re-render

Background:
I have a ListBox containing items defined by DataTemplates. Right now, if an object in the list has the property IsEditable set to true, the item's property information will be displayed inside of textboxes (via DataTemplate change), instead of textblocks (so the user can edit the content of that list item)
IsEditable is toggled on/off by a button inside of each list item. I have been told that we need to keep the state of all objects consistent, which means I can't just rebind the ItemsSource and lose everything.
Currently, I'm using this to re-render:
this.lbPoints.Dispatcher.Invoke(DispatcherPriority.Render, new Action(() => { }));
Question:
The aforementioned code snippet KIND OF does its job. By "kind of", I mean, it does eventually cause my data to become re-rendered, but only when I scroll to the bottom of the list and then scroll back up to the item i'm trying to re-render.
1) How can I re-render the data immediately without having to scroll around to get it to show up?
The guys commenting are right that you're going about this the wrong way... there is rarely a need to force a ListBox to re-render. You're probably causing yourself some additional grief trying to switch the DataTemplates (although it is possible). Instead of that, think about data binding the TextBox.IsReadOnly property to your IsEditable property:
<TextBox IsReadOnly="{Binding IsEditable}" Text="{Binding Text}" />
Another alternative is to use a BooleanToVisibilityConverter to show a different Grid in your DataTemplate when your IsEditable property is true. Unfortunately, that Converter doesn't have an inverse operation, so you could create an IsNotEditing property to bind to the Grid in the DataTemplate that is originally displayed. I'm not sure if that's clear... see this example:
<DataTemplate DataType="{x:Type YourPrefix:YourDataType}">
<Grid>
<Grid Visibility="{Binding IsNotEditing, Converter={StaticResource
BooleanToVisibilityConverter}}">
<!-- Define your uneditable UI here -->
</Grid>
<Grid Visibility="{Binding IsEditing, Converter={StaticResource
BooleanToVisibilityConverter}}">
<!-- Define your editable UI here -->
</Grid>
</Grid>
</DataTemplate>
You could also define your own BooleanToVisibilityConverter class that has an IsInverted property, so that you can just use the one IsEditing property. You'd need to declare two Converters still, like this:
<Converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<Converters:BoolToVisibilityConverter x:Key="InvertedBoolToVisibilityConverter"
IsInverted="True" />
Then your XAML would be like this:
<Grid Visibility="{Binding IsEditing, Converter={StaticResource
InvertedBoolToVisibilityConverter}}">
<!-- Define your uneditable UI here -->
</Grid>
<Grid Visibility="{Binding IsEditing, Converter={StaticResource
BoolToVisibilityConverter}}">
<!-- Define your editable UI here -->
</Grid>

Adding custom control to listbox

I have a custom control ListItem. I need to display five such items in a window and these items could change during runtime; items could be added or deleted, or content could change in ListItem.
ListBox appears to be a good solution to display items. But what I have seen is we can add items and style them, and can handle updates with data trigger.
myListBox.Items.Add(new { FileName = "SomeFile", State="Uploaded" });
But we can not do something like
ListItem curItem = new ListItem();
myListBox.Items.Add(new { curItem });
Even if I do it shows empty item in the list.
So if I want to add my custom control to some listbox, how could that be possible. That is using ListBox just as a container so we can get away from the pain of positioning and all that after list changes. Or is there a better way to do that?
You are in luck - this is the bread and butter of WPF! Set the ItemsSource of your ListBox (possible in XAML or cs):
myListBox.ItemsSource = myEnumerableCollection;
or
<ListBox ItemsSource="{Binding MyItemsProperty}">
Use a DataTemplate (you do not need a UserControl) to style each item in XAML:
<ListBox ItemsSource="{Binding MyItemsProperty}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding FileName}"/>
<TextBlock Text="{Binding State}"/>
<!--Whatever you want-->
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
If your collection is an ObservableCollection<T> changes to that collection (e.g. items added or removed) will be reflected in the ListBox automatically. If T implements INotifyPropertyChanged changes to properties on each item will also automatically show up on the UI.
For more see the WPF Binding Overview.
Don't create or manipulate UI elements in procedural code in WPF.
<ListBox ItemsSource="{Binding SomeCollection}">
<ListBox.ItemTemplate>
<DataTemplate>
<my:MyControl/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
where my:MyControl is a UserControl with whatever UI you want.

Since a selection list, how can I set the navigation sequentially?

I'm starting a new project in WPF and am now looking into using Prism. For now I'm simply trying to set up the navigation of the application using Prism. Unfortunately, my lack of experience with the framework makes it a bit difficult to get started.
To be more precise about my first challenge I have an application with a "navigation/menu" region and a "main" region.
In "navigation/menu" region, I have several checkboxes, in this case we have four of them, which represents a sequential navigation. I.E. we've selected View 2 and View 4.
So, when the user click Start, in "main" region must appear each view selected in that order. Check the below image, View 2 is first. Then when the user press next, must show View 4.
I mean on a more structural level..
if I could only get through the first steps..
Prism support TabControl Region Adapter, navigation can be done using standard requestNavigation method.
You need add all your tab content using Region.Add method to the region in your module's init phase.
view:
<TabControl prism:RegionManger.RegionName="tabRegion" />
C# code:
IRegionManager manager;
manager.Regions["tabRegion"].Views.Add(Container.Resolve(typeof(YourViewType)));
In your viewModel, you should write you navigation command:
public void NextView() {
regionManager.RequestNavigation("tabRegion", new Uri("YourViewType", UriKind.Relative));
}
bind to your "next" button:
<Button Command="{Binding NextViewCommand}" />
If you want to control whether user can navigate to next page, you can implement INavigationAware interface.
If you don't want lost data between navigation, you can make your view model has ContainerMangedLifeCycle or implement IsNavigationTarget method to return true.
Sorry for untested code sample, but you should get the point.
Create a class named ViewVM with a property IsSelected. Must implement INotifyPropertyChanged.
Add an ObservableCollection<View> named Views to your datacontext. Populate it with new instances of ViewVM.
Put an ItemsControl in your Window, with ItemsSource set to Views. The DataTemplate for the ItemsControl items should contain a CheckBox (with IsChecked bound to IsSelected) and a Label.
Add a TabControl to your Window, with ItemSource set to Views. Add a Style for TabItem such that TabItems are only visible if IsSelected is true.
Following the above steps will give you a window containing a list of views with checkboxes, as you requested, and a TabControl displaying only the selected views. Below is the XAML (I have tested this):
<StackPanel>
<ItemsControl ItemsSource="{Binding Path=Views}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding Path=IsSelected}"></CheckBox>
<TextBlock Text="{Binding Path=Title}"></TextBlock>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TabControl ItemsSource="{Binding Path=Views}">
<TabControl.ItemTemplate>
<DataTemplate>
<Grid>
<TextBlock Text="{Binding Path=Title}"></TextBlock>
</Grid>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.Resources>
<Style TargetType="TabItem">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsSelected}" Value="False">
<Setter Property="Visibility" Value="Collapsed"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</TabControl.Resources>
</TabControl>
</StackPanel>
This addresses the structural/design aspect and should give you a good start to creating your solution - you'll also need to create a custom control to use instead of the TabControl. Instead of having tabs, your custom control should contain Next and Previous buttons to navigate between views.

Categories

Resources