Silverlight TabItem Visibility not changing - silverlight

I have a TabControl with many TabItems binding to a ViewModel that has properties for each TabItem's Visibility.
<sdk:TabControl>
<sdk:TabItem Name="Inventory" Header="Inventory"
Style="{StaticResource TabItemStyle}"
Visibility="{Binding Permissions.Inventory,
Converter={StaticResource PermissiveVisibilityConverter},
ConverterParameter='Viewer'}"
DataContext="{Binding VM}" />
</sdk:TabControl>
All TabItems are defaulted to a Visibility of collapsed. But when the VM changes a TabItem to Visible it does not work until you move your mouse over the control...
Even if I set the visibility programmatically with a button it behaves the same way!
I have checked to see if the VM's properties are Notifying the UI, and they are with NotifyOnPropertyChanged. And if I bind the data to a button's visibility it works just fine...It is just the TabItems that seem to have a bug.
Is there a way to get the TabItem UI to refresh? Or a work-around for this?
Thanks!

I've faced the same problem and I was able to overcome it using a attached "Visibility" property instead the original one. In this new property, I can pass the value to the original "Visibility" property and, in case the parent tab control's "SelectedItem" is being collapsed select the next visible tabItem.
However, as noted here, only that may not be enough if the first item is collapsed when the TabControl loads. This case had to be fixed in the TabControl itself, because tests showed when the fake "Visility" is set for the first time the TabItem does not have access to it's TabControl yet. Because of that I also used a attached property for the TabControl which corrects this issue.
The complete solution:
public static class TabControlExtensions
{
/// <summary>
/// Use this property on a TabControl to correct the behavior
/// of selecting Collapsed TabItems.
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public static bool GetSelectOnlyVisibleTabs(DependencyObject obj)
{
return (bool)obj.GetValue(SelectOnlyVisibleTabsProperty);
}
public static void SetSelectOnlyVisibleTabs(DependencyObject obj, bool value)
{
obj.SetValue(SelectOnlyVisibleTabsProperty, value);
}
public static readonly DependencyProperty SelectOnlyVisibleTabsProperty =
DependencyProperty.RegisterAttached("SelectOnlyVisibleTabs", typeof(bool), typeof(TabControlExtensions), new PropertyMetadata(false, SelectOnlyVisibleTabsChanged));
public static void SelectOnlyVisibleTabsChanged(object sender, DependencyPropertyChangedEventArgs args)
{
var tabControl = sender as TabControl;
if (tabControl == null) return;
if ((bool)args.NewValue)
{
tabControl.SelectionChanged += TabControl_SelectionChanged;
CorrectSelection(tabControl);
}
else
{
tabControl.SelectionChanged -= TabControl_SelectionChanged;
}
}
private static void TabControl_SelectionChanged(object sender, SelectionChangedEventArgs args)
{
var tabControl = sender as TabControl;
if (tabControl == null) return;
CorrectSelection(tabControl);
}
public static void CorrectSelection(TabControl tabControl)
{
var selected = tabControl.SelectedItem as UIElement;
if (selected == null) return;
// If the selected element is not suposed to be visible,
// selects the next visible element
if (selected.Visibility == System.Windows.Visibility.Collapsed)
tabControl.SelectedItem = tabControl.Items.OfType<UIElement>()
.Where(e => e.Visibility == System.Windows.Visibility.Visible)
.FirstOrDefault();
}
}
public static class TabItemExtensions
{
/// <summary>
/// Use this property in a TabItem instead of the original "Visibility" to
/// correct the behavior of a TabControl when a TabItem's Visibility changes.
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public static Visibility GetVisibility(DependencyObject obj)
{
return (Visibility)obj.GetValue(VisibilityProperty);
}
public static void SetVisibility(DependencyObject obj, Visibility value)
{
obj.SetValue(VisibilityProperty, value);
}
public static readonly DependencyProperty VisibilityProperty =
DependencyProperty.RegisterAttached("Visibility", typeof(Visibility), typeof(TabItemExtensions), new PropertyMetadata(Visibility.Visible, VisibilityChanged));
public static void VisibilityChanged(object sender, DependencyPropertyChangedEventArgs args)
{
var tabItem = sender as TabItem;
if (tabItem == null) return;
var visibility = (Visibility)args.NewValue;
if (tabItem.Visibility == visibility) return;
tabItem.Visibility = visibility;
if (visibility == Visibility.Visible) return;
// Finds the tab's parent tabcontrol and corrects the selected item,
// if necessary.
var tabControl = tabItem.Ancestors().OfType<TabControl>().FirstOrDefault();
if (tabControl == null) return;
TabControlExtensions.CorrectSelection(tabControl);
}
}
The usage:
<sdk:TabControl local:TabControlExtensions.SelectOnlyVisibleTabs="True">
<sdk:TabItem Header="tabItem1" Visibility="Collapsed">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="TabItem1 which should not be visible (1)" />
</sdk:TabItem>
<sdk:TabItem Header="tabItem2">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="TabItem2 which should be visible (2)" />
</sdk:TabItem>
<sdk:TabItem DataContext="{Binding ViewModel}"
Header="tabItem3"
local:TabItemExtensions.Visibility="{Binding MyProperty,
Converter={StaticResource BoolToVisibilityConverter}}">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="TabItem with binded Visibility (3)" />
</sdk:TabItem>
</sdk:TabControl>

What if you change the xaml so that you're setting the DataContext first:
<sdk:TabItem Name="Inventory" Header="Inventory"
Style="{StaticResource TabItemStyle}"
DataContext="{Binding VM}"
Visibility="{Binding Permissions.Inventory,
Converter={StaticResource PermissiveVisibilityConverter},
ConverterParameter='Viewer'}" />
I'm assuming that Permissions.Inventory is a property on your view model but since you haven't set the context at that point it seems like the binding shouldn't work.
Also, is your converter being hit if you set a breakpoint and if the getter on Permissions.Inventory being called?

Related

Property bound to DependencyProperty won't update despite TwoWay Binding set

I have a little problem here. I've created custom TreeView using RadTreeView. It all works nice, but I've encountered an obstacle. I've set DependencyProperty for SelectedItem in TreeView. I nest my control in View, bind property to SelectedItem in TwoWay mode, but bound property won't update, it's null all the time, despite DependencyProperty value being set.
Here's tree xaml:
<Grid xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:sdk='http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk'
xmlns:telerik='http://schemas.telerik.com/2008/xaml/presentation' x:Name='this' >
<Grid.Resources>
<DataTemplate x:Key='ChildTemplate'>
<TextBlock Text='{Binding Path=ChildPath}' Margin='5,0' />
</DataTemplate>
<telerik:HierarchicalDataTemplate x:Key='NameTemplate' ItemsSource='{Binding ChildrenCollectionPath}' ItemTemplate='{StaticResource ChildTemplate}'>
<TextBlock Text='{Binding Path=ParentPath }' Padding='7'/>
</telerik:HierarchicalDataTemplate>
</Grid.Resources>
<telerik:RadTreeView x:Name='rtvTreeView' Padding='5' BorderThickness='0' IsEditable='False' IsLineEnabled='True' IsExpandOnDblClickEnabled='False' ItemTemplate='{StaticResource NameTemplate}' />
</Grid>
Below is way I nest the control in View:
<windows:TreeViewReuse CollectionSource="{Binding SitesCollectionWithAddress}" ParentPath="Napis" Grid.Column="0" BorderThickness="2" SelectedItemD="{Binding SelectedSide, ElementName=this, UpdateSourceTrigger=Explicit, Mode=TwoWay}" ChildPath="FullAddress" ChildrenCollectionPath="AdresyStrony" BorderBrush="Red" DoubleClickCommand="{Binding TreeViewDoubleClick}">
</windows:TreeViewReuse>
And here's Tree's code behind in parts:
public partial class TreeViewReuse : UserControl
{
static Telerik.Windows.FrameworkPropertyMetadata propertyMetaData = new Telerik.Windows.FrameworkPropertyMetadata(null,
Telerik.Windows.FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, new PropertyChangedCallback(SelectedItemChangedCallback));
public object SelectedItemD
{
get { return GetValue(SelectedItemDProperty); }
set { SetValue(SelectedItemDProperty, value); }
}
public static readonly DependencyProperty SelectedItemDProperty =
DependencyProperty.Register("SelectedItemD", typeof(object), typeof(TreeViewReuse), propertyMetaData);
public TreeViewReuse()
{
InitializeComponent();
Loaded += new RoutedEventHandler(TreeViewReuse_Loaded);
}
void treeView_SelectionChanged(object sender, Telerik.Windows.Controls.SelectionChangedEventArgs e)
{
SelectedItemD = _treeView.SelectedItem;
}
static private void SelectedItemChangedCallback(DependencyObject dp, DependencyPropertyChangedEventArgs e)
{
}
Does anyone have an idea why property bound to SelectedItemD does not update? I don't care about setting tree's selected item from it, I only want to set it to selected item.
Here's property:
public StronaSprawy SelectedSide
{
get
{
return _selectedSide;
}
set
{
_selectedSide = value;
}
}
Your Dependency Property looks fine.. all except for that Telerik.Windows.FrameworkPropertyMetadata instance.
Silverlight does not support setting meta data options, so I cant think how the Telerik implementation will achieve that. It is possible that Telerik have their own DP implementation, or even that this type of property meta data only works with their controls.
Try using the standard System.Windows.PropertyMetaData type instead and see if that works for you.

Silverlight TextBox VisibilityChanged event

I'm working on a Windows Phone app using Silverlight C# and XAML. My page contains a ListBox which renders a list of databound objects that the user can manipulate, i.e. add/rename/delete.
I've got it working that the add/rename of items is done in-place, i.e. by swapping a TextBlock for a TextBox depending on the state of the object (bool IsEditable property) and making use of a parameterized VisibilityConverter to manage the opposite Visibility states.
<UserControl.Resources>
<local:VisibilityConverter x:Key="VisibilityConverter" True="Visible" False="Collapsed"/>
<local:VisibilityConverter x:Key="InvertedVisibility" True="Collapsed" False="Visible"/>
</UserControl.Resources>
...
<TextBlock Text="{Binding Name}" Visibility="{Binding IsEditable, Converter={StaticResource InvertedVisibility}}" />
<TextBox Text="{Binding Name}" Visibility="{Binding IsEditable, Converter={StaticResource VisibilityConverter}}"/>
The thing is that I also want the TextBox to automatically grab focus when it becomes visible, so that the on-screen keyboard pops up without the user having to tap the TextBox.
Since there's no VisibilityChanged event on a regular TextBox, I subclassed TextBox to TextBox2 and added my own:
public class TextBox2 : TextBox
{
public TextBox2()
{
DefaultStyleKey = typeof(TextBox);
}
public static readonly DependencyProperty VisibilityChangedProperty = DependencyProperty.Register(
"VisibilityChanged",
typeof(string),
typeof(TextBox2),
new PropertyMetadata("Set the VisibilityChanged event handler"));
public event VisibilityChangedEventHandler VisibilityChanged;
public delegate void VisibilityChangedEventHandler(object sender, EventArgs e);
public new Visibility Visibility
{
get
{
return base.Visibility;
}
set
{
if (base.Visibility != value)
{
base.Visibility = value;
VisibilityChanged(this, new EventArgs());
}
}
}
}
Now my XAML looks like this:
<TextBlock Text="{Binding Name}" Visibility="{Binding IsEditable, Converter={StaticResource InvertedVisibility}}"/>
<local:TextBox2 Text="{Binding Name}" Visibility="{Binding IsEditable, Converter={StaticResource VisibilityConverter}}" VisibilityChanged="ListEdit_VisibilityChanged"/>
And the event handler like this:
void ListEdit_VisibilityChanged(object sender, EventArgs e)
{
TextBox textBox = (TextBox)sender;
if (textBox.Visibility == System.Windows.Visibility.Collapsed)
return;
textBox.Focus();
}
The TextBox2 renders properly and behaves just like a TextBox at runtime, but my VisibilityChanged event handler is not firing when the databinding flips the value of IsEditable.
IsEditable defines the Visibility and the TextBox2 does become visible correctly, so the databinding is working.
I can cause the event to fire programmatically by getting hold of the TextBox2 instance and setting the Visibility of that in code. That also works.
But this databinding scenario being responsible for setting the Visibility seems not to work.
Any ideas why not?
Here are 2 solutions that I use.
Solution 1 needs no sub class, but solution 2 is more reusable.
1. You can subscribe to the Loaded event of the TextBox, and force a focus, like so:
void TextBox_Loaded_Focus(object sender, System.Windows.RoutedEventArgs e) {
ForceFocusControl((Control)sender);
}
void ForceFocusControl(Control control) {
control.Focus();
if (FocusManager.GetFocusedElement() != control) {
Dispatcher.BeginInvoke(() => ForceFocusControl(control));
}
}
This solution goes into a recursive loop though, you might want to add some checks to make it safer.
2. Keep your subclass TextBox2, and rather create a private MyVisibility dependency property that you bind to the Visibility property of the base class, but also specify a DependencyProperty_Changed handler, like so:
/// <summary>
/// <see cref="TextBox2"/> will focus itself when it becomes visible.
/// </summary>
public sealed class TextBox2 : TextBox {
public TextBox2() {
SetBinding(TextBox2.MyVisibilityProperty, new Binding("Visibility") { Source = this });
}
static readonly DependencyProperty MyVisibilityProperty = DependencyProperty.Register(
/* name = */ "MyVisibilityProperty",
/* property type = */ typeof(Visibility),
/* owner type = */ typeof(TextBox2),
/* meta = */ new PropertyMetadata(MyVisibilityProperty_Changed));
static void MyVisibilityProperty_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e) {
TextBox2 TextBox2 = (TextBox2)d;
if (TextBox2.Visibility == Visibility.Visible) {
TextBox2.Focus();
}
}
}
This is how my TextBox2 class looks now:
public class TextBox2 : TextBox
{
public event VisibilityChangedEventHandler VisibilityChanged;
public delegate void VisibilityChangedEventHandler(object sender, EventArgs e);
public static readonly DependencyProperty VisibilityChangedProperty = DependencyProperty.Register(
"VisibilityChanged", typeof(VisibilityChangedEventHandler), typeof(TextBox2), null);
static readonly DependencyProperty MirrorVisibilityProperty = DependencyProperty.Register(
"MirrorVisibility", typeof(Visibility), typeof(TextBox2), new PropertyMetadata(MirrorVisibilityChanged));
public TextBox2()
{
SetBinding(TextBox2.MirrorVisibilityProperty, new Binding("Visibility") { Source = this });
}
static void MirrorVisibilityChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
((TextBox2)obj).VisibilityChanged(obj, null); // raise event
}
}

Textbox in listview - update source and move focus on tab not working at same time

I have a listview. I have set following int that :-
<ListView KeyboardNavigation.TabNavigation="Local" SelectionMode="Extended">
<ListView.ItemContainerStyle>
<Style>
<Setter Property="KeyboardNavigation.IsTabStop" Value="False"/>
</Style>
</ListView.ItemContainerStyle>
One column in listview contains TextBox's.
If I set the UpdateSourceTrigger=LostFocus
in my textbox, I can not tab through the listview...Instead if I set UpdateSourceTrigger=Explicit, the tabbing is working...but source will not get updated.
Please help me
EDIT
public class TextBoxBehavior
{
#region Attached Property EscapeClearsText
public static readonly DependencyProperty EscapeClearsTextProperty
= DependencyProperty.RegisterAttached("EscapeClearsText", typeof(bool), typeof(TextBoxBehavior),
new FrameworkPropertyMetadata(false, new PropertyChangedCallback(OnEscapeClearsTextChanged)));
private static void OnEscapeClearsTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if ((bool)e.NewValue)
{
var textBox = d as TextBox;
if (textBox != null)
{
textBox.KeyUp -= TextBoxKeyUp;
textBox.KeyUp += TextBoxKeyUp;
}
}
}
private static void TextBoxKeyUp(object sender, KeyEventArgs e)
{
if (e.Key == Key.Escape)
{
//((DataContext<string>)((TextBox)sender).GetBindingExpression(TextBox.TextProperty).DataItem).RollbackChanges();
((TextBox)sender).Text = string.Empty;
}
else if (e.Key == Key.Enter)
{
((TextBox)sender).GetBindingExpression(TextBox.TextProperty).UpdateSource();
}
}
public static void SetEscapeClearsText(DependencyObject dependencyObject, bool escapeClearsText)
{
if (!ReferenceEquals(null, dependencyObject))
dependencyObject.SetValue(EscapeClearsTextProperty, escapeClearsText);
}
public static bool GetEscapeClearsText(DependencyObject dependencyObject)
{
if (!ReferenceEquals(null, dependencyObject))
return (bool)dependencyObject.GetValue(EscapeClearsTextProperty);
return false;
}
#endregion Attached Property EscapeClearsText
}
Below is the listview/gridview column which has the attached property in it.
<GridViewColumn Width="60">
<GridViewColumnHeader Content="Priority"
Command="{Binding Path=SortSelectedClaimCodeGroupsCommand}"
CommandParameter="Item.IntPriority">
</GridViewColumnHeader>
<GridViewColumn.CellTemplate>
<DataTemplate>
<Border DataContext="{Binding Item.Priority}"
Style="{StaticResource ValidationResultBorderStyle}" HorizontalAlignment="Left" >
<TextBox Width="200" MaxLength="25" Text="{Binding Path=Value,Mode=TwoWay,
UpdateSourceTrigger=Explicit}" local:TextBoxBehavior.EscapeClearsText="True" >
When you set the UpdateSourceTrigger as explicit, you have to update the source by explicitly calling the method UpdateSource on your BindingExpression. Where is the code for that?
EDIT
In your TextBoxKeyUp event you are overwriting your Binding by setting the text on the press of Escape key. Firstly you bind it to the property Value and later you are explicitly setting the Textbox text property to String.Empty.This way text property will loose it's binding. So, later whenever you call UpdateSource it won't propagate to Source value since it's no longer binded to the Text property of textbox. Instead you should set the text like this -
((TextBox)sender).SetCurrentValue(TextBox.TextProperty, String.Empty);
This way your binding will be preserved and UpdateSource would work as it should.

Databinding TextBlock Runs in Silverlight / WP7

I'm using Silverlight on Windows Phone 7.
I want to display the first part of some text in a TextBlock in bold, and the rest in normal font. The complete text must wrap. I want the bolded part to contain text from one property in my ViewModel, and the plain text to contain text from a different property.
The TextBlock is defined in a DataTemplate associated with a LongListSelector.
My initial attempt was:
<TextBlock TextWrapping="Wrap">
<TextBlock.Inlines>
<Run Text="{Binding Property1}" FontWeight="Bold"/>
<Run Text="{Binding Property2}"/>
</TextBlock.Inlines>
</TextBlock>
This fails at runtime with the spectacularly unhelpful "AG_E_RUNTIME_MANAGED_UNKNOWN_ERROR". This is a known issue because the Run element is not a FrameworkElement and cannot be bound.
My next attempt was to put placeholders in place, and then update them in code:
<TextBlock Loaded="TextBlockLoaded" TextWrapping="Wrap">
<TextBlock.Inlines>
<Run FontWeight="Bold">Placeholder1</Run>
<Run>Placeholder2</Run>
</TextBlock.Inlines>
</TextBlock>
In the code-behind (yes I am desparate!):
private void TextBlockLoaded(object sender, RoutedEventArgs e)
{
var textBlock = (TextBlock)sender;
var viewModel = (ViewModel)textBlock.DataContext;
var prop1Run = (Run)textBlock.Inlines[0];
var prop2Run = (Run)textBlock.Inlines[1];
prop1Run.Text = viewModel.Property1;
prop2Run.Text = viewModel.Property2;
}
This seemed to work, but because I am using the LongListSelector, although items get recycled, the Loaded codebehind event handler doesn't re-initialize the Runs, so very quickly the wrong text is displayed...
I've looked at using the LongListSelector's Linked event (which I already use to free up images that I display in the list), but I can't see how I can use that to re-initialize the Runs' text properties.
Any help appreciated!
I finally found a solution that works for me.
As I mention in the comment, Paul Stovell's approach would not work.
Instead I used a similar approach to add an attached property to the TextBlock, bound to the TextBlock's DataContext, and attached properties on the runs, indicating which ViewModel properties they should be bound to:
<TextBlock TextWrapping="Wrap"
Views:BindableRuns.Target="{Binding}">
<TextBlock.Inlines>
<Run FontWeight="Bold" Views:BindableRuns.Target="Property1"/>
<Run Views:BindableRuns.Target="Property2"/>
</TextBlock.Inlines>
</TextBlock>
Then in my attached TextBox Target (datacontext) property's changed event, I update the Runs, and subscribe to be notified of changes to the TextBox Target properties. When a TextBox Target property changes, I updated any associated Run's text accordingly.
public static class BindableRuns
{
private static readonly Dictionary<INotifyPropertyChanged, PropertyChangedHandler>
Handlers = new Dictionary<INotifyPropertyChanged, PropertyChangedHandler>();
private static void TargetPropertyPropertyChanged(
DependencyObject dependencyObject,
DependencyPropertyChangedEventArgs e)
{
if(!(dependencyObject is TextBlock)) return;
var textBlock = (TextBlock)dependencyObject;
AddHandler(e.NewValue as INotifyPropertyChanged, textBlock);
RemoveHandler(e.OldValue as INotifyPropertyChanged);
InitializeRuns(textBlock, e.NewValue);
}
private static void AddHandler(INotifyPropertyChanged dataContext,
TextBlock textBlock)
{
if (dataContext == null) return;
var propertyChangedHandler = new PropertyChangedHandler(textBlock);
dataContext.PropertyChanged += propertyChangedHandler.PropertyChanged;
Handlers[dataContext] = propertyChangedHandler;
}
private static void RemoveHandler(INotifyPropertyChanged dataContext)
{
if (dataContext == null || !Handlers.ContainsKey(dataContext)) return;
dataContext.PropertyChanged -= Handlers[dataContext].PropertyChanged;
Handlers.Remove(dataContext);
}
private static void InitializeRuns(TextBlock textBlock, object dataContext)
{
if (dataContext == null) return;
var runs = from run in textBlock.Inlines.OfType<Run>()
let propertyName = (string)run.GetValue(TargetProperty)
where propertyName != null
select new { Run = run, PropertyName = propertyName };
foreach (var run in runs)
{
var property = dataContext.GetType().GetProperty(run.PropertyName);
run.Run.Text = (string)property.GetValue(dataContext, null);
}
}
private class PropertyChangedHandler
{
private readonly TextBlock _textBlock;
public PropertyChangedHandler(TextBlock textBlock)
{
_textBlock = textBlock;
}
public void PropertyChanged(object sender,
PropertyChangedEventArgs propertyChangedArgs)
{
var propertyName = propertyChangedArgs.PropertyName;
var run = _textBlock.Inlines.OfType<Run>()
.Where(r => (string) r.GetValue(TargetProperty) == propertyName)
.SingleOrDefault();
if(run == null) return;
var property = sender.GetType().GetProperty(propertyName);
run.Text = (string)property.GetValue(sender, null);
}
}
public static object GetTarget(DependencyObject obj)
{
return obj.GetValue(TargetProperty);
}
public static void SetTarget(DependencyObject obj,
object value)
{
obj.SetValue(TargetProperty, value);
}
public static readonly DependencyProperty TargetProperty =
DependencyProperty.RegisterAttached("Target",
typeof(object),
typeof(BindableRuns),
new PropertyMetadata(null,
TargetPropertyPropertyChanged));
}
I suggest you give the BindableRun a try. I've only used it in WPF, but I don't see why it wouldn't work in Silverlight.

How to Achieve Lazy Binding of Tab Page Controls in WPF?

I have an entity class. This entity has lots of properties and entity's data is shown to the user in several TabItems of a TabControl. I also implement MVVM approach.
When the screen is shown to the user first, I want to bind only the active tab page controls and as the user navigates through tab pages additional separate bindings will be incurred as-needed. How can I achieve that?
You don't have anything to do, that's the default behavior. The DataTemplate for a TabItem content won't be instantiated until this TabItem is selected
EDIT: here's an example:
<Window.Resources>
<DataTemplate DataType="{x:Type vm:Page1ViewModel}">
<v:Page1View />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:Page3ViewModel}">
<v:Page3View />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:Page3ViewModel}">
<v:Page3View />
</DataTemplate>
</Window.Resources>
...
<TabControl ItemsSource="{Binding Pages}"
DisplayMemberPath="Title">
</TabControl>
In the code above, the TabControl will pick the appropriate DataTemplate based on the item type, and will render it only when that item is selected.
EDIT 2: apparently you want to display the data of a single ViewModel on several pages. If you want the controls of each TabItem to lazily instantiated, you need to use the ContentTemplate property of each TabItem:
<TabControl>
<TabItem Header="Page 1">
<TabItem.ContentTemplate>
<DataTemplate>
<v:Page1View />
</DataTemplate>
</TabItem.ContentTemplate>
</TabItem>
<TabItem Header="Page 2">
<TabItem.ContentTemplate>
<DataTemplate>
<v:Page2View />
</DataTemplate>
</TabItem.ContentTemplate>
</TabItem>
<TabItem Header="Page 3">
<TabItem.ContentTemplate>
<DataTemplate>
<v:Page3View />
</DataTemplate>
</TabItem.ContentTemplate>
</TabItem>
</TabControl>
I created this solution which works for our third party tabcontrol.
The idea is to "intercept" the datacontext when it is beeing set, save it for later and set the datacontext back to null. When the tabitem gets focus we then set the datacontext and the data will populate in the tab.
Implemented as a dependency property. Then simply set the property on the tabs that need to (do not set it on the tab that comes up as a default)
#region SavedDataContext
private static object GetSavedDataContext(TabItemEx tabItem)
{
return tabItem.GetValue(SavedDataContextProperty);
}
private static void SetSavedDataContext(TabItemEx tabItem, object value)
{
tabItem.SetValue(SavedDataContextProperty, value);
}
public static readonly DependencyProperty SavedDataContextProperty =
DependencyProperty.RegisterAttached("SavedDataContext", typeof(object),
typeof(Attach), new UIPropertyMetadata(null));
#endregion
#region LazyLoad
public static bool GetLazyLoad(TabItemEx tabItem)
{
return (bool)tabItem.GetValue(LazyLoadProperty);
}
public static void SetLazyLoad(TabItemEx tabItem, bool value)
{
tabItem.SetValue(LazyLoadProperty, value);
}
private static readonly DependencyProperty LazyLoadProperty =
DependencyProperty.RegisterAttached("LazyLoad", typeof(bool),
typeof(Attach), new UIPropertyMetadata(false, LazyLoadPropertyChanged));
private static void LazyLoadPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs eventArgs)
{
if ((bool)eventArgs.NewValue)
{
var tabItemEx = sender as TabItemEx;
if (tabItemEx == null)
return;
tabItemEx.DataContextChanged += DataContextChanged;
tabItemEx.GotFocus += TabGotFocus;
}
}
#endregion
private static void TabGotFocus(object sender, RoutedEventArgs e)
{
var tabItemEx = sender as TabItemEx;
if (tabItemEx == null)
return;
tabItemEx.GotFocus -= TabGotFocus;
tabItemEx.DataContext = GetSavedDataContext(tabItemEx);
tabItemEx.IsSelected = true;
}
private static void DataContextChanged(object sender, DependencyPropertyChangedEventArgs eventArgs)
{
var tabItemEx = sender as TabItemEx;
if (tabItemEx == null)
return;
SetSavedDataContext(tabItemEx, eventArgs.NewValue);
tabItemEx.DataContextChanged -= DataContextChanged;
tabItemEx.DataContext = null;
}
Marked as answer approach has one drawback - content of TabItem will be always re-rendered when selected. If it's critical - you can try this.

Resources