Passing the value of an item selected in a Gridview to the ViewModel of a different Usercontrol - silverlight

After researching a solution to this for three days, finally I gave up. Now I need your help to solve this.
Scenario:
I've a GridView in a Usercontrol (Lets say WLMSLogs.xaml) and My GridView ItemSource is binded to a List from the ViewModel (WMLSLogsViewModel.cs)
Lets say the List has 4 items (EventID, Name, Request and Response). BothRequest and Responses are XML Strings.
GridView needs to display some of the List items in RowDetailsTemplate under different tab items. So I'm displaying Request and Response under respective TabItems.
<GridView x:Name="WMLSLogGrid" ItemsSource="{Binding WMLSLogModelList}">
<GridView.Columns>
<GridViewDataColumn DataMemberBinding="{Binding ID}" Header="ID"/>
<GridViewDataColumn DataMemberBinding="{Binding UserName}" Header="UserName"/>
</GridView.Columns>
<GridView.RowDetailsTemplate>
<DataTemplate >
<TabControl>
<TabItem Header="Request Xml">
<TextBlock text="{Binding Request}"/>
</TabItem>
<TabItem Header="Response Xml">
<TextBlock text="{Binding Response}"/>
</TabItem>
<TabItem Header="EventLogs" Tag="{Binding .}">
<views:LogEvents />
</TabItem>
</TabControl>
</Grid>
</DataTemplate>
</GridView.RowDetailsTemplate>
WMLSLogModelList is a Observable collection in WMLSLogsViewModel.cs
So far everything works fine and Grid is displaying data as expected.
Now When User expands any row, he can see two TabItems with request and response.
Here I need to add one more TabItem (LogEvents) besides Request and Response tabs.
This LogEvents tab is going have one more GridView to display (so I added a new View <views:LogEvents /> in the tab). Here comes the tricky part.
This LogEvents GridView needs to get the data based on the corresponding Selecteditem (which is EventId), and pass this EventId to a different ViewModel (LogEventViewModel.cs) and binds the data to the Inner GridView dynamically. All this has to happen either as I expand the RowDetails section or if I select the Tab LogEvents.
There is no relation between the data items of these two Grids, except getting the Selected EventId of the main GridView and passing this to a different ViewModel then to Domain service to get the inner GridView Data.
What I did so far
As I mentioned I created a new View UserControl for LogEvents, Placed it under new TabItem(EventLogs) inside row details template of main GridView.
LogEvent UserControl contains a Grid View binded to LogEventsViewModel to get the Collection based on Selected row EventId.
How do I assign the Selected EventId to a new ViewModel and Get the data dynamically?
One Way: As I showed you, I called LogEvents by placing it in side TabItem. whenever I expanded any row, Then It is Initializing the LogEvents page, During that I tried to bind the Datacontext to LogEventsViewModel. But I'm unable to get the Seleted row EventId dynamically. If I get that then I can easily pass it to the LogEventsViewModel constructor.
var _viewModel = new LogEventsViewModel(EventId);
this.DataContext = _viewModel;
InitializeComponent();
Other way:
Pass the selected EventId directly from xaml View binding to that page initialization and then to LogEventsViewModel
Something like this
<views:LogEvents something="{Binding EventId}"/>
Is there any other way to do this?
Details:
LogEvents.xaml
<UserControl.Resources>
<viewModel:LogEventsViewModel x:Key="ViewModel"/>
</UserControl.Resources>
<Grid DataContext="{Binding Source={StaticResource ViewModel}}">
<GridView x:Name="LogEventsGrid" ItemsSource="{Binding View}"
<GridView.Columns>
<telerik:GridViewToggleRowDetailsColumn />
<telerik:GridViewDataColumn DataMemberBinding="{Binding EventId}" Header="LogEventId"/>
<telerik:GridViewDataColumn DataMemberBinding="{Binding Exception}" Header="Exception" />
</GridView.Columns>
</telerik:RadGridView>
</Grid>
LogEvents.xaml.cs
public int EventId
{
get { return (int)GetValue(EventIdProperty); }
set { SetValue(EventIdProperty, value); }
}
public static readonly DependencyProperty EventIdProperty =
DependencyProperty.Register("EventId", typeof(int), typeof(ApplicationLog),
new PropertyMetadata(0, new PropertyChangedCallback(OnEventIdChanged)));
private static void OnEventIdChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
int LogEventId1 = (int)e.NewValue;
// Need to assign propery in LogEventsViewModel
}
LogEventsViewModel.cs
WMLCDomainContext _Context = new WMLCDomainContext();
private QueryableDomainServiceCollectionView<LogEvents> view;
public int _eventid;
public ApplicationLogsViewModel()
{
EntityQuery<LogEvents> getLogEventsQuery = _Context.GetApplicationLogListQuery(EventId);
this.view = new QueryableDomainServiceCollectionView<ApplicationLog>(_Context, getLogEventsQuery );
}
public IEnumerable View
{get {return this.view;}}
public int EventId
{
get{return this._eventid;}
set{_eventid = value;}
}

I would create a dependency property on your LogEventsViewModel and then set up a binding on your LogEvents view, something like this:
<views:LogEvents EventId="{Binding EventId}" />
Then in LogEvents.xaml.cs you could create your dependency property:
private LogEvents_ViewModel _viewModel
{
get { return this.DataContext as LogEvents_ViewModel; }
}
public string EventId
{
get { return (string)GetValue(EventIdProperty); }
set { SetValue(EventIdProperty, value); }
}
public static readonly DependencyProperty EventIdProperty =
DependencyProperty.Register("EventId", typeof(string), typeof(LogEvents),
new PropertyMetadata(string.Empty, new PropertyChangedCallback(OnEventIdChanged)));
private static void OnEventIdChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((LogEvents)d).OnTrackerInstanceChanged(e);
}
protected virtual void OnEventIdChanged(DependencyPropertyChangedEventArgs e)
{
this._viewModel.EventId = e.NewValue;
}

KodeKreachor is correct, though it may be necessary to set a Bindable attribute on your exposed property. Without it, a property might not always show in the bindable properties of the control, even if it does still work.

Related

WPF ListView Measure and Arrange issue with bound ItemsSource vs manually added items

I have a series of UserControls intended for printing purposes only. They are mainly tables implemented with ListView & GridView. Each control has a backing view model, and the ItemSource of the ListView is bound to the view model. All pretty standard. When I actually print these controls to PDF, they display the bound data without an issue. The trouble arises when I am trying to Measure and Arrange the individual controls so that I can determine how many rows will fit on that page.
<UserControl>
<Grid>
<ListView x:Name="ItemsListView" ItemsSource="{Binding Items}">
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn Header="Column Name 1" DisplayMemberBinding="{Binding Column_Property_1}" />
<GridViewColumn Header="Column Name 2" DisplayMemberBinding="{Binding Column_Property_2}" />
</GridView.Columns>
</GridView>
</ListView.View>
</ListView>
</Grid>
</UserControl>
public partial class MyControl : UserControl
{
private readonly ViewModel _viewModel;
public MyControl()
{
InitializeComponent();
}
public MyControl(ViewModel viewModel) :this()
{
_viewModel = viewModel;
this.DataContext = _viewModel;
}
}
public class ViewModel
{
public IEnumerable<MyItem> Items { get; set; }
}
public class MyItem
{
public string Column_Property_1 { get; set; }
public string Column_Property_2 { get; set; }
}
When I Measure and Arrange the control in the pagination service, the ActualHeight of the control is always { 0, 0 }.
public Size GetMeasuredSize(UserControl control)
{
control.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
control.Arrage(new Rect(control.DesiredSize));
control.UpdateLayout();
return new Size(control.ActualWidth, control.ActualHeight);
}
What I have found though, by inspecting the control in the debugger during the GetMeasureSize method, is that the Items property on the ListView control is not populated at all by the binding- the count is 0. So it makes sense that its desired size and actual sizes are 0, it just doesn't have any items to dispay.
If I remove the binding and manually add each item from my view model, it's a completely different story, and I get the correct ActualWidth and ActualHeight from the control.
public MyControl(ViewModel viewModel) :this()
{
_viewModel = viewModel;
this.DataContext = _viewModel;
foreach (var item in _viewModel.Items)
{
ItemsListView.Items.Add(item);
}
}
Obviously, that is a little less than desirable. So I'm hoping that someone can either help explain this behavior and point me in the right direction to get this working with the bindings.
I've tried "force updating" the binding in the MeasureOverride method
ItemsListView.GetBindingExpression(ListView.ItemsSourceProperty).UpdateTarget();
BindingOperations.GetBindingExpression(ItemsListView, ListView.ItemsSourceProperty).UpdateTarget();
Neither resulted in the items property being populated.
Update:
I've just found this:
Customized Measure / Arrange algorithms on controls that use Data Binding in WPF
which sheds some light on the situation.

databinding datagrid's comboboxes and textboxes to staticresource list

I've created a WPF vb.net form containing datagrid. Inside of datagrid I have two comboboxes and two textboxes, one combobox textbox pair for articles and another one for services. I should bind first combo to property of List(Of article) type and second to List(Of service) type where article is public class containing two public properties (articleId and articleName) and service is public class containing two public properties (serviseId and serviceName). Textboxes should display article and service names and comboboxes should display IDs. When combo selection is changed textbox text should change its value too.
List(Of article) and List(Of service) should be populated from database.
How could I do this, I know that solution is somewhere around me but can't catch it at all. There are two main problems, binding controls and populatin list from database.
If I need to post a part of code I'm gonna do this, just let me know.
Please help me to solve this situation,
thanks.
First of all - sorry, I am not very familiar with VB.Net, so my code is written in c#. Since I have no understanding of what you actually need, I can only offer you a following not really elegant solution.
For binding your ComboBox to the items from database you need to use a StaticResource, as I can see you did it, but maybe you defined it wrong, that's why it is not working for you. To get it work you need to have a class like this:
public class artiklsList : List<artikl>
{
public artiklsList()
{
this.Add(new artikl(1, "first")); //this is dummy items, you need to do a database stuff here
this.Add(new artikl(2, "second"));
this.Add(new artikl(3, "third"));
}
}
And a xaml like this:
<Window.Resources>
<my:artiklsList x:Key="source"></my:artiklsList>
</Window.Resources>
You also need update a cell with text when a ComboBox selection changed. It's not a simple task, because the easiest way to do this using Binding to the control using ElementName will not work in the DataGrid. What I did is kinda hacky anyway...
So, xaml is not very complicated:
<DataGrid Name="dgrStavke" AutoGenerateColumns="False" Height="160" Width="600" HorizontalAlignment="Left" Margin="5" Grid.Row="7" Grid.ColumnSpan="4" >
<DataGrid.Columns>
<DataGridTemplateColumn Header="Artikl ID">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox SelectedIndex="{Binding selectedIndexID, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Name="cmbArtikli" Width="120" DisplayMemberPath="artiklId" ItemsSource="{StaticResource source}">
</ComboBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Naziv artikla" Binding="{Binding nazivArtikla}"/>
</DataGrid.Columns>
</DataGrid>
The binding of SelectedIndex and a horrible code-behind doing the trick with updating a text cell. For binding to work properly articl class have to implement INotifyPropertyChanged interface.
public class artikl: INotifyPropertyChanged
{
public artikl(int artid, string nazivart)
{
artiklId = artid;
nazivArtikla = nazivart;
}
public int artiklId{get;set;}
private string _nazv;
public string nazivArtikla
{
get { return _nazv; }
set { _nazv = value; NotifyPropertyChanged("nazivArtikla"); }
}
//Here I think you may have questions
private int _index;
public int selectedIndexID
{
get
{
//To get a SelectedIndex for ComboBox in current row we look in
//listArtikli defined in a MainWindow for a articli item with a current
//item's Id and take the index of this item
artikl art = MainWindow.listArtikli.Find(el => el.artiklId == this.artiklId);
return MainWindow.listArtikli.IndexOf(art);
}
set
{
//This property is binded with SelectedIndex property of ComboBox.
//When selected index changed, we look in listArtikli and take
//here properties of item with this index.
//This will change values of item binded to the current grid row
_index = value;
this.nazivArtikla = MainWindow.listArtikli[value].nazivArtikla;
this.artiklId = MainWindow.listArtikli[value].artiklId;
NotifyPropertyChanged("selectedIndexID");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
And a window code-behind:
public partial class MainWindow : Window
{
public static artiklsList listArtikli = new artiklsList();
public static artiklsList gridsource = new artiklsList();
public MainWindow()
{
InitializeComponent();
dgrStavke.ItemsSource = gridsource;
}
}
You need gridsource aside from listArtikli to fill your DataGrid with values. Also because of all this code in selectedIndexID if you use only listArtikli its values corrupting. So listArtikli contains articli items just sorted in order which they were retrieved. And gridsource contains pairs artiklId-nazivArtikla as they presented in the DataGrid.
Hope it will help you just a little.
thanks for interesting. If you need more clarification please let me know. This is the code for one pair combobox-textbox, I'll easily apply this for another one:
XAML
...
<DataGrid Name="dgrStavke" AutoGenerateColumns="False" Height="160" Width="600" HorizontalAlignment="Left" Margin="5" Grid.Row="7" Grid.ColumnSpan="4">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Artikl ID">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox Name="cmbArtikli" Width="120" ItemsSource="{Binding Source={StaticResource artcls}, Path=listArtikli}" DisplayMemberPath="artiklId"></ComboBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Naziv artikla" Binding="{Binding nazivArtikla}"></DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
...
CODE
Imports System.Data
Imports System.Data.SqlClient
Imports System.Text
Namespace MW
Public Class artikl
Sub New(artid As Integer, nazivart As String)
' TODO: Complete member initialization
artiklId = artid
nazivArtikla = nazivart
End Sub
Public Property artiklId() As Integer
Public Property nazivArtikla() As String
End Class
Public Class frmDodavanjePaketa
Public Property listArtikli() As New List(Of artikl)
Private Sub popuniComboArtikli()
Dim sqlConn As SqlConnection = New SqlConnection(moduleGlobal.connString)
sqlConn.Open()
Dim strSql As New StringBuilder
strSql.Append("select a.artiklId, a.nazivArtikla ")
strSql.Append(" from artikli a ")
strSql.Append(" where isnull(a.aktivan, 0) = 1")
Dim sqlCom As SqlCommand = New SqlCommand(strSql.ToString, sqlConn)
Dim sqlDs As DataSet = New DataSet
Dim sqlDa As SqlDataAdapter = New SqlDataAdapter
sqlDa.SelectCommand = sqlCom
sqlDa.Fill(sqlDs)
For Each row As DataRow In sqlDs.Tables(0).Rows
Me.listArtikli.Add(New artikl(row.ItemArray(0).ToString, row.ItemArray(1).ToString))
Next
End Sub
End Class
End Namespace

Why does the ComboBox in my DataGrid column reset itself to null?

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...

access TextBoxes inside DataTemplate

I know question similar to this has asked several times in SO. But non of them address my issue and have some difficulty with understanding those answers. This is my situation; I have a ItemsControl I have used ItemTemplate and bound some data.
<Window.Resources>
<DataTemplate x:Key="AdditionalFieldTemlate">
<Grid>
<TextBlock Text="{Binding InfoName}"/>
<TextBox Text="{Binding InfoValue,Mode=TwoWay}" Name="CustomValue"/>
</Grid>
</DataTemplate>
</Window.Resources>
<Grid>
<ItemsControl ItemsSource="{Binding AdditionalInformation}" x:Name="additionalInfo" ItemTemplate="{DynamicResource AdditionalFieldTemlate}"/>
</Grid>
I need to set TextBox text to empty(all the textboxes text inside datatemplate) once click on a Button. Don't know how to access these textboxes. Please help me.
You don't normally access the TextBoxes (the look) ....you access the data that is being bound to.
So you can just alter the "data" in your collection as follows:
foreach (var item in AdditionalInformation)
{
item.InfoValue = "";
}
The "TextBoxes" will then be emptied.
Make sure you have implemented INotifyPropertyChanged on the class being used by AdditionalInformation....so that when the InfoValue property is altered it raises a notification.
The text in the textboxes is databound to the InfoValue property of your class. Implement the class and proprty like this:
class InfoClass: INotifyPropertyChanged
{
private string _infoValue;
...
public string InfoValue
{
get { return _infoValue; }
set
{
_infoValue = value;
OnNotifyPropertyChanged("InfoValue")
}
}
...
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string property)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
Then do what colinsmith suggests in your button click handler (or command if you went with the MVVM approach). The binding will be notified to the change and the view will be updated.

TabControl ItemTemplate without ItemsSource

I am doing a WPF application with a TabControl. At the beginning I had a TabControl bound to ObservableCollection of TabBase items, where TabBase is a base class for tab viewmodel:
<TabControl
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding Tabs}"
ItemTemplate="{StaticResource ClosableTabTemplate}"
...
public ObservableCollection<TabBase> Tabs { get; private set; }
...
public abstract class TabBase : ViewModelBase
...
public abstract class ViewModelBase : INotifyPropertyChanged
{
public virtual string DisplayName { get; protected set; }
...
<DataTemplate x:Key="ClosableTabTemplate">
<DockPanel Width="120">
<Button
Command="{Binding Path=CmdClose}"
Content="X"
/>
<ContentPresenter
Content="{Binding Path=DisplayName}">
</ContentPresenter>
</DockPanel>
</DataTemplate>
But I've faced with an issue when I switch tabs it looks like current tab is being created each time, even if it was already opened before. Searching thru StackOverflow I've found the solution here with reference to here. I've replaced using of declarative ItemsSource with dynamic creation of tabs from code. Tabs switching performance issue was resolved, but tab headers have lost link to template, so instead of tab header with caption and close button I see just a little tab header without anything. Playing a bit with tab creation code, I was able to restore tab size and close button, but without binding - there is no caption and close button doesn't work (5 lines with item.Header restored original tab size):
private void AddTabItem(TabBase view)
{
TabItem item = new TabItem();
item.DataContext = view;
item.Content = new ContentControl();
(item.Content as ContentControl).Focusable = false;
(item.Content as ContentControl).SetBinding(ContentControl.ContentProperty, new Binding());
item.Header = new ContentControl();
(item.Header as ContentControl).DataContext = view;
(item.Header as ContentControl).Focusable = false;
(item.Header as ContentControl).SetBinding(ContentControl.ContentProperty, new Binding());
item.HeaderTemplate = (DataTemplate)FindResource("ClosableTabTemplate");
tabControl.Items.Add(item);
}
The question is, how can I make ItemTemplate working for TabControl without ItemsSource binding?
When you explicitly set your item.Header to a ContentControl, the HeaderTemplate is now using that object as its DataContext. Normally, the Header property would get your ViewModel and a ContentPresenter would take that (non-Visual) object and apply the HeaderTemplate to it. You've now pushed your ViewModel down a level in the hierarchy so the template is not being applied at the same place as the data. Moving either one should fix the Binding issues but one or the other may work better for your situation:
item.Header = view;
or
(item.Header as ContentControl).ContentTemplate = (DataTemplate)FindResource("ClosableTabTemplate");

Resources