Как отсоединиться от дерева визуалов в WPF
Я пытаюсь правильно удалить UIElement
из InlineUIContainer
чтобы использовать его в другой Panel, но программа продолжает аварийно завершать работу с этим сообщением "Указанный Visual уже является потомком другого Visual или корнем CompositionTarget.".
Я создал небольшое приложение, чтобы проиллюстрировать мою боль. В этой программе, после того, как Рэнди Баттон убил \ удалил его подруга, он все еще не отрывается от своего родителя, которого я узнал, UIElementIsland
, И затем любая попытка добавить Рэнди в качестве потомка чего-либо еще приводит к сбою приложения (кнопка "Апокалипсис" подтверждает мою точку зрения:)). Вы можете нажать, чтобы проверить родителей Рэнди до \ после удаления Рэнди, чтобы заметить, что он постоянно находится под UIElementIsland
как ребенок, если он отстранен, вся проблема \ apocalypse должна быть предотвращена.
Это забавное приложение, поэтому копируйте и компилируйте, даже если это просто для удовольствия! Любая помощь \ идеи будут оценены!
Часть C#:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;
namespace DetachingfromUIElementIsland
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
int t = 0;
static string[] info = new string[] { "Okay, Lets have a look...", "Checking."
, "Checking..", "Checking...", "Seen it!" };
/// <summary>
/// Makes the App fancy :)
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void timer_Tick(object sender, EventArgs e)
{
display.Text = info[t];
if (t == 0)
timer.Interval = new TimeSpan(0, 0, 0, 0, 300);
t++;
if (t >= 4)
{
t = 0;
timer.Stop();
display.Text = GetRandysParent();
}
}
private void deleteRandy_Click(object sender, RoutedEventArgs e)
{
// This might be the bug.
// Maybe there's a better way to do this.
// If there was a VisualTreeHelper.Remove().
randy_container.Child = null;
display.Text = "Haha! I just killed Randy!!! He'll never get the chance"
+ "\n to hurt another woman again!";
display.Background = Brushes.Violet;
end.Visibility = System.Windows.Visibility.Visible;
}
DispatcherTimer timer = null;
/// <summary>
/// Check if Randy is Still attached to UIElementIsland
/// </summary>
/// <returns></returns>
private string GetRandysParent()
{
// Check the visual tree to see if randy is removed properly
DependencyObject dp = VisualTreeHelper.GetParent(randy);
string text = string.Empty;
if (dp != null)
{
display.Background = Brushes.LightGreen;
text = "Randy's Dad is Mr " + dp.ToString();
}
else
{
// This should be what you'll get when the code works properly
display.Background = Brushes.Red;
text = "Weird...Randy doesn't seem to have a dad...";
}
return text;
}
private void findParents_Click(object sender, RoutedEventArgs e)
{
display.Background = Brushes.Yellow;
// Creates a timer to display some fancy stuff
// and then Randy's.
// Just to prove to you that this button actually works.
timer = new DispatcherTimer();
timer.Start();
timer.Tick += timer_Tick;
timer.Interval = new TimeSpan(0, 0, 0, 0, 700);
}
private void randy_Click(object sender, RoutedEventArgs e)
{
// Get Randy to introduce himself
display.Text = "Hi, I'm Randy!!!";
display.Background = Brushes.Orange;
}
private void end_Click(object sender, RoutedEventArgs e)
{
// If randy is removed properly, this would not crash the application.
StackPanel s = new StackPanel();
s.Children.Add(randy);
// CRASH!!!
}
}
}
XAML:
<Window x:Class="DetachingfromUIElementIsland.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<FlowDocument IsEnabled="True" x:Name="document">
<Paragraph>
<InlineUIContainer x:Name="randy_container">
<!--Meet Randy-->
<Button Name="randy" Content="I am a Randy, the button" Click="randy_Click" ToolTip="Meet Randy"/>
</InlineUIContainer>
<LineBreak/>
<LineBreak/>
<InlineUIContainer x:Name="container2">
<!--Meet Randy's Ex Girlfriend-->
<Button Name="deleteRandy" Content="Randy dumped me for another girl :(, click me to delete him" Click="deleteRandy_Click" ToolTip="Meet Randy's Ex Girlfriend"/>
</InlineUIContainer>
<LineBreak/>
<LineBreak/>
<InlineUIContainer x:Name="container3">
<!--He can help you find Randy's Parents-->
<Button Name="findParents" Content="Click me to find randy's parents" Click="findParents_Click" ToolTip="He can help you find Randy's Parents"/>
</InlineUIContainer>
<LineBreak/>
<LineBreak/>
<InlineUIContainer x:Name="Apocalypse">
<!--End the world, Crash the application-->
<Button x:Name="end" Content="Avenge Randy's Death" Click="end_Click" ToolTip="End the world, Crash the application" Visibility="Hidden"/>
</InlineUIContainer>
</Paragraph>
<Paragraph>
<InlineUIContainer>
<TextBlock x:Name="display" Foreground="Black"/>
</InlineUIContainer>
</Paragraph>
</FlowDocument>
</Window>
Весь код должен был быть короче этого, но я добавил его, чтобы сделать его немного веселее. Надеюсь, я немного скрасил чей-то день. Но все же, помоги мне:).
Ответ: Получите от Рэнди InlineUIContainer
следующее:
public class DerivedInlineUIContainer : InlineUIContainer
{
public DerivedInlineUIContainer()
{
}
public void RemoveFromLogicalTree(FrameworkElement f)
{
this.RemoveLogicalChild(f);
}
}
Теперь вы могли бы убить Рэнди на этот раз и добавить его в небеса StackPanel
):
randy_container.RemoveFromLogicalTree(randy);
IDisposable disp = VisualTreeHelper.GetParent(randy) as IDisposable;
if (disp != null)
disp.Dispose();
// Poor Randy is going to heaven...
StackPanel heaven = new StackPanel();
heaven.add(randy);
Спасибо всем.
2 ответа
Удаление визуального родителя, кажется, не помогает:
private void end_Click(object sender, RoutedEventArgs e)
{
IDisposable disp = VisualTreeHelper.GetParent(randy) as IDisposable;
if (disp != null)
disp.Dispose();
DependencyObject parent = VisualTreeHelper.GetParent(randy);
if (parent == null)
MessageBox.Show("No parent");
// If randy is removed properly, this would not crash the application.
StackPanel s = new StackPanel();
s.Children.Add(randy);
}
Таким образом, вы можете создать новый Button
:
public MainWindow()
{
InitializeComponent();
randy_container.Child = CreateRandyButton();
}
private void end_Click(object sender, RoutedEventArgs e)
{
StackPanel s = new StackPanel();
s.Children.Add(CreateRandyButton());
}
private Button CreateRandyButton()
{
Button button = new Button { Name = "randy", Content = "I am a Randy, the button", ToolTip = "Meet Randy" };
button.Click += randy_Click;
return button;
}
... или просто спрячьте, как предложено @Sinatr.
Я нашел обходной путь, если родитель все еще UIElementIsland
. Поскольку он реализуетIDisposable
, вы можете очистить его дочерние элементы таким образом:
var parent = VisualTreeHelper.GetParent(element);
if (parent is IDisposable uiElementIsland)
{
uiElementIsland.Dispose();
}
Это неприятно, но работает.
Это смешно, но также очень шумно. Вы получите ответ намного быстрее, если ваша демонстрация будет короткой.
Вместо удаления / добавления визуала вы можете просто скрыть / показать его:
void deleteRandy_Click(object sender, RoutedEventArgs e) =>
randy.Visibility = Visibility.Hidden;
void end_Click(object sender, RoutedEventArgs e) =>
randy.Visibility = Visibility.Visible;
Таким образом, вы не играете с визуальным деревом невосстановимым способом. Вы можете использовать шаблоны данных MVVM + или x:Shared=False
ресурсы, если вы действительно хотите удалить элемент пользовательского интерфейса, а затем добавить новый.