I ended up modifying my OnItemsChanged
event to run the Remove code at a lower dispatcher priority then the Add code, so it gives the Add operation a chance to cancel the Remove one and reuse the TabItem's ContentPresenter instead of rendering a new one.
Original code I started with was obtained from here
It basically stores the TabItem ContentPresenters so when switching tabs it uses a stored ContentPresenter instead of redrawing a new one. Here's the modifications I made to OnItemsChanged to get the Drag/Drop to reuse an old item instead of redrawing a new one
case NotifyCollectionChangedAction.Add:
case NotifyCollectionChangedAction.Remove:
// Search for recently deleted items caused by a Drag/Drop operation
if (e.NewItems != null && _deletedObject != null)
{
foreach (var item in e.NewItems)
{
if (_deletedObject == item)
{
// If the new item is the same as the recently deleted one (i.e. a drag/drop event)
// then cancel the deletion and reuse the ContentPresenter so it doesn't have to be
// redrawn. We do need to link the presenter to the new item though (using the Tag)
ContentPresenter cp = FindChildContentPresenter(_deletedObject);
if (cp != null)
{
int index = _itemsHolder.Children.IndexOf(cp);
(_itemsHolder.Children[index] as ContentPresenter).Tag =
(item is TabItem) ? item : (this.ItemContainerGenerator.ContainerFromItem(item));
}
_deletedObject = null;
}
}
}
if (e.OldItems != null)
{
foreach (var item in e.OldItems)
{
_deletedObject = item;
// We want to run this at a slightly later priority in case this
// is a drag/drop operation so that we can reuse the template
// Render is good since a normal Removal of an item will run prior to adding a new one
this.Dispatcher.BeginInvoke(DispatcherPriority.Render,
new Action(delegate()
{
if (_deletedObject != null)
{
ContentPresenter cp = FindChildContentPresenter(_deletedObject);
if (cp != null)
{
this._itemsHolder.Children.Remove(cp);
}
}
}
));
}
}