【问题标题】:How to detach from the Visual Tree in WPF如何从 WPF 中的可视化树中分离
【发布时间】:2017-08-21 09:47:41
【问题描述】:

我正在尝试从 InlineUIContainer 中正确删除 UIElement 以便在另一个面板中使用它,但程序不断崩溃并显示此消息“指定的视觉对象已经是另一个视觉对象的子对象或构图目标。”。

我创建了一个小应用程序来说明我的痛苦。在这个程序中,一旦 Randy 按钮被他的女朋友杀死\删除,他仍然没有脱离他的父母,我发现他是 UIElementIsland。然后任何尝试将 Randy 添加为其他任何东西的孩子都会使应用程序崩溃(天启按钮证明了我的观点 :))。您可以在删除 Randy 之前/之后点击查看 Randy 的父母,注意他小时候一直在UIElementIsland 下,如果他被分离,整个问题\应该避免。

这是一个有趣的应用程序,即使只是为了好玩,也要复制和编译!任何帮助\想法将不胜感激!

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>

整个代码应该比这更短,但我对其进行了添加以使其更有趣。希望我能照亮某人的一天。但是,请帮助我:)。

答案: 源自 Randy 的InlineUIContainer 如下:

    public class DerivedInlineUIContainer : InlineUIContainer
    {   
        public DerivedInlineUIContainer()
        {

        }

        public void RemoveFromLogicalTree(FrameworkElement f)
        {
            this.RemoveLogicalChild(f);
        }
    }

现在你可以正确地杀死 Randy,并将他添加到 UIElement 天堂(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);

谢谢大家。

【问题讨论】:

  • “加香料让它变得有趣”在这里是弄巧成拙的。改为发布MCVE
  • 我认为您还必须从其父级(即段落)中删除 InlineUiContainer。我不知道为什么,但我的代码中有一个扩展方法可以做到这一点,因为几个月前我遇到了同样的问题。
  • 注意到这一点。下次我会弥补的。谢谢@Clemens
  • 不起作用。刚刚试过@Lennart
  • 请注意,您仍然可以编辑您的问题...

标签: c# wpf xaml visualtreehelper inlineuicontainer


【解决方案1】:

删除视觉父级似乎没有帮助:

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 的建议隐藏它。

【讨论】:

  • 是的,它仍然在崩溃,但消息已更改为“指定元素已经是另一个元素的逻辑子元素。先断开它。”。我会看看如何解决这个问题
  • 当你这样做时它不应该崩溃:s.Children.Add(CreateRandyButton());
  • 嘿!干得好!!!我从InlineUIContainer 派生出来并称为RemoveLogicalChild()。连同您对Dispose() 的建议。它奏效了。非常感谢!
【解决方案2】:

这很有趣,但也很吵。如果你的演示很短,你会更快得到答案。

您可以简单地隐藏/显示它,而不是删除/添加视觉对象:

void deleteRandy_Click(object sender, RoutedEventArgs e) =>
    randy.Visibility = Visibility.Hidden;

void end_Click(object sender, RoutedEventArgs e) =>
    randy.Visibility = Visibility.Visible;

这样你就不会以不可恢复的方式玩视觉树。如果您确实想删除 UI 元素然后添加 新的,可以使用 MVVM + 数据模板或 x:Shared=False 资源。

【讨论】:

  • 听起来不错,但这只是一个演示。我想在富文本框中使用逻辑。
【解决方案3】:

我找到了一种解决方法,以防父级仍然是 UIElementIsland。由于它实现了IDisposable,因此您可以通过这种方式清除其子代:

var parent = VisualTreeHelper.GetParent(element);
if (parent is IDisposable uiElementIsland)
{
   uiElementIsland.Dispose();
}

这不是很好,但它有效。

【讨论】:

    猜你喜欢
    • 2021-12-18
    • 2023-04-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-10-20
    • 2012-02-28
    • 2021-08-04
    • 2019-11-18
    相关资源
    最近更新 更多