wpf 将弹出窗口位置锁定到元素,或在 ItemsControl 中伪造带有图层的弹出窗口以进行就地编辑

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/15371659/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-13 08:03:16  来源:igfitidea点击:

Locking popup position to element, or faking a popup with layers for in-place editing in an ItemsControl

wpfpopupz-indexedit-in-place

提问by Jamiec

What I am trying to achieve is essentially in-place editing of a databound object inside an ItemsControlin wpf.

我想要实现的本质上是ItemsControl在 wpf 中就地编辑数据绑定对象。

my ItemsControlis a horizontal WrapPanelcontaining multiple instances of a usercontrol (NameControl) which displays as a little pink Glyph with a person's name. It looks like this

myItemsControl是一个WrapPanel包含用户控件 ( NameControl) 的多个实例的水平对象,它显示为带有人名的粉红色小字形。看起来像这样

Fig1

图。1

With a popup I am able to show an editor for this "Name" (Other properties of the bound object things like Address,Genderetc.) and this works absoluttely fine. My XAML at this point would be along the lines of

一个弹出对话框,我能够证明这个“名”(其他像绑定对象的事情性质的编辑器AddressGender等等),这工作absoluttely罚款。我的 XAML 在这一点上将沿着

<Style x:Key="NamesStyle" TargetType="{x:Type ItemsControl}">
    <Setter Property="ItemsPanel">
        <Setter.Value>
            <ItemsPanelTemplate>
                <WrapPanel Orientation="Horizontal" />
            </ItemsPanelTemplate>
        </Setter.Value>
    </Setter>
    <Setter Property="ItemTemplate">
        <Setter.Value>
            <DataTemplate>
            <StackPanel>
                <Button Command="{Binding EditName}" BorderThickness="0" Background="Transparent" Panel.ZIndex="1">
                    <widgets:NameControl />
                </Button>
                <Popup IsOpen="{Binding IsEditMode}"
                            PlacementTarget="{Binding ElementName=button}"
                            Margin="0 5 0 0" Placement="Relative" AllowsTransparency="True" >

                <Border Background="White" BorderBrush="DarkOrchid" BorderThickness="1,1,1,1" CornerRadius="5,5,5,5" 
                        Panel.ZIndex="100">
                    <Grid ShowGridLines="False" Margin="5" Background="White" Width="300">
                        <!-- Grid Content - just editor fields/button etc -->
                    </Grid>
                </Border>
                </Popup>
            </StackPanel>
        </DataTemplate>
        </Setter.Value>
    </Setter>
</Style>

Giving an output when I click a Name looking like

当我点击一个看起来像的名字时给出一个输出

Fig2

图2

With this look im quite happy (apart from my awful choice of colours!!) except that the popup does not move with the widow (resize/minimize/maximize) and that popup is above everythingeven other windows.

这一下IM很高兴(除了我的色彩选择,可怕!)除了弹出不与寡妇(调整大小/最小化/最大化),以及弹出式移动高于一切,甚至其他窗口。

So one way to solve part of that is to "attach" or lock the popup position to the element. I have not found a good/easy/xaml way to do that. Ive come across a few code-based solutions but im not sure I like that. It just has a bit of a smell about it.

因此,解决部分问题的一种方法是“附加”或锁定元素的弹出位置。我还没有找到一个好的/简单/xaml 的方法来做到这一点。我遇到了一些基于代码的解决方案,但我不确定我喜欢那个。它只是有一点气味。

Another solution ive tried to achieve is to ditch the popup and try to emulate the behaviour of a layer/panel that sits above the other names but is position over (or below, im not fussy) the associated name control.

我试图实现的另一个解决方案是放弃弹出窗口并尝试模拟位于其他名称上方但位于相关名称控件上方(或下方,我并不挑剔)的图层/面板的行为。

Ive tried a few different things, mainly around setting Panel.ZIndexon controls within a PanelControl(The Grid, the WrapPanel, a DockPanel on the very top of my MainWindow) with little success. I have implemented a simple BoolToVisibilityConverterto bind my editor Grid's Visibilityproperty to my IsEditModeview model property and that works fine, but I cant for the life of me arrange my elements in the ItemsControlto show the editor grid over the names.

我尝试了一些不同的方法,主要是围绕Panel.ZIndexPanelControl我的 MainWindow 顶部的控件(网格、WrapPanel、DockPanel)中设置控件,但收效甚微。我已经实现了一个简单BoolToVisibilityConverter的将我的编辑器网格的Visibility属性绑定到我的IsEditMode视图模型属性的方法,并且效果很好,但是我一生都无法将元素排列在名称上ItemsControl以显示编辑器网格。

To do what is described above I simply commented out the Popupand added the following binding to the Borderwhich contains the editor grid Visibility="{Binding IsEditMode, Converter={StaticResource boolToVisibility}}".

为了执行上述操作,我简单地注释掉了Popup并将以下绑定添加到Border包含编辑器网格的Visibility="{Binding IsEditMode, Converter={StaticResource boolToVisibility}}"

All that does is this:

所做的就是这样:

Fig3

图3

It just shows the popup underthe name but not overthe others.

它只是显示弹出名字,但不是其他人。

Any help? What am I doing wrong?

有什么帮助吗?我究竟做错了什么?

回答by Mike Fuchs

Sounds like a job for the AdornerLayer to me.

对我来说,这听起来像是 AdornerLayer 的工作。

My implementation will just display one 'popup' at a time, and you can hide it by clicking the button another time. But you could also add a small close button to the ContactAdorner, or stick with your OK button, or fill the AdornerLayer behind the ContactAdorner with an element that IsHitTestVisible and reacts on click by hiding the open Adorner (so clicking anywhere outside closes the popup).

我的实现一次只显示一个“弹出窗口”,您可以再次单击该按钮将其隐藏。但是你也可以给 ContactAdorner 添加一个小的关闭按钮,或者坚持你的 OK 按钮,或者用 IsHitTestVisible 的元素填充 ContactAdorner 后面的 AdornerLayer 并通过隐藏打开的 Adorner 来响应点击(所以点击外面的任何地方都会关闭弹出窗口) .

Edit: Added the small close button at your request. Changes in ContactAdorner and the ContactDetailsTemplate.

编辑:根据您的要求添加了小的关闭按钮。ContactAdorner 和 ContactDetailsTemplate 中的更改。

Another thing that you might want to add is repositioning of the adorner once it is clipped from the bottom (I only check for clipping from the right).

您可能想要添加的另一件事是从底部剪下装饰器后重新定位它(我只检查从右侧剪下)。

enter image description here

在此处输入图片说明

Xaml:

Xml:

<UserControl x:Class="WpfApplication1.ItemsControlAdorner"
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
                 xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
                 xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
                 mc:Ignorable="d" 
                 xmlns:local="clr-namespace:WpfApplication1"
                 d:DesignHeight="300" d:DesignWidth="300">

    <UserControl.DataContext>
        <local:ViewModel />
    </UserControl.DataContext>

    <UserControl.Resources>
        <local:EnumToBooleanConverter x:Key="EnumToBooleanConverter" />

        <!-- Template for the Adorner -->
        <DataTemplate x:Key="ContactDetailsTemplate" DataType="{x:Type local:MyContact}" >
            <Border Background="#BBFFFFFF" BorderBrush="DarkOrchid" BorderThickness="1" CornerRadius="5" TextElement.Foreground="DarkOrchid" >
                <Grid Margin="5" Width="300">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="Auto" />
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*"/>
                        <ColumnDefinition Width="*"/>
                        <ColumnDefinition Width="Auto"/>
                    </Grid.ColumnDefinitions>
                    <TextBlock Text="Full name" />
                    <TextBox Grid.Row="1" Text="{Binding FullName, UpdateSourceTrigger=PropertyChanged}" />
                    <TextBlock  Grid.Row="2" Text="Address" />
                    <TextBox Grid.Row="3" Grid.ColumnSpan="2" Text="{Binding Address}" />
                    <TextBlock Grid.Column="1" Text="Gender" />
                    <StackPanel Orientation="Horizontal" Grid.Column="1" Grid.Row="1" >
                        <RadioButton Content="Male" IsChecked="{Binding Gender, Converter={StaticResource EnumToBooleanConverter}, ConverterParameter={x:Static local:Gender.Male}}" />
                        <RadioButton Content="Female" IsChecked="{Binding Gender, Converter={StaticResource EnumToBooleanConverter}, ConverterParameter={x:Static local:Gender.Female}}" />
                    </StackPanel>
                    <Button x:Name="PART_CloseButton" Grid.Column="2" Height="16">
                        <Button.Template>
                            <ControlTemplate>
                                <Border Background="#01FFFFFF" Padding="3" >
                                    <Path Stretch="Uniform" ClipToBounds="True" Stroke="DarkOrchid" StrokeThickness="2.5" Data="M 85.364473,6.9977109 6.0640998,86.29808 6.5333398,85.76586 M 6.9926698,7.4977169 86.293043,86.79809 85.760823,86.32885"  />
                                </Border>
                            </ControlTemplate>
                        </Button.Template>
                    </Button>
                </Grid>
            </Border>
        </DataTemplate>

        <!-- Button/Item style -->
        <Style x:Key="ButtonStyle1" TargetType="{x:Type Button}" >
            <Setter Property="Foreground" Value="White" />
            <Setter Property="FontFamily" Value="Times New Roman" />
            <Setter Property="Background" Value="#CC99E6" />
            <Setter Property="BorderThickness" Value="0" />
            <Setter Property="MinHeight" Value="24" />
            <Setter Property="Margin" Value="3,2" />
            <Setter Property="Padding" Value="3,2" />
            <Setter Property="Border.CornerRadius" Value="8" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="Button">
                        <Border CornerRadius="{TemplateBinding Border.CornerRadius}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}" Margin="{TemplateBinding Margin}" >
                            <ContentPresenter VerticalAlignment="Center" HorizontalAlignment="Center" />
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

        <!-- ItemsControl style -->
        <Style x:Key="NamesStyle" TargetType="{x:Type ItemsControl}">
            <Setter Property="ItemsPanel">
                <Setter.Value>
                    <ItemsPanelTemplate>
                        <WrapPanel Orientation="Horizontal" />
                    </ItemsPanelTemplate>
                </Setter.Value>
            </Setter>
            <Setter Property="ItemTemplate">
                <Setter.Value>
                    <DataTemplate>
                        <Button x:Name="button" Style="{StaticResource ButtonStyle1}" Content="{Binding FullName}" >
                            <i:Interaction.Behaviors>
                                <local:ShowAdornerBehavior DataTemplate="{StaticResource ContactDetailsTemplate}" />
                            </i:Interaction.Behaviors>
                        </Button>
                    </DataTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </UserControl.Resources>

    <Grid>
        <ItemsControl ItemsSource="{Binding MyContacts}" Style="{StaticResource NamesStyle}" />
    </Grid>

</UserControl>

ShowAdornerBehavior, ContactAdorner, EnumToBooleanConverter:

ShowAdornerBehavior、ContactAdorner、EnumToBooleanConverter:

using System.Windows;
using System.Linq;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Interactivity;
using System.Windows.Media;
using System.Windows.Data;
using System;
namespace WpfApplication1
{
    public class ShowAdornerBehavior : Behavior<Button>
    {
        public DataTemplate DataTemplate { get; set; }

        protected override void OnAttached()
        {
            this.AssociatedObject.Click += AssociatedObject_Click;
            base.OnAttached();
        }

        void AssociatedObject_Click(object sender, RoutedEventArgs e)
        {
            var adornerLayer = AdornerLayer.GetAdornerLayer(this.AssociatedObject);
            var contactAdorner = new ContactAdorner(this.AssociatedObject, adornerLayer, this.AssociatedObject.DataContext, this.DataTemplate);
        }
    }

    public class ContactAdorner : Adorner
    {
        private ContentPresenter _contentPresenter;
        private AdornerLayer _adornerLayer;
        private static Button _btn;
        private VisualCollection _visualChildren;

        private double _marginRight = 5;
        private double _adornerDistance = 5;
        private PointCollection _points;

        private static ContactAdorner _currentInstance;

        public ContactAdorner(Button adornedElement, AdornerLayer adornerLayer, object data, DataTemplate dataTemplate)
            : base(adornedElement)
        {
            if (_currentInstance != null)
                _currentInstance.Hide(); // hides other adorners of the same type

            if (_btn != null && _btn == adornedElement)
            {
                _currentInstance.Hide(); // hides the adorner of this button (toggle)
                _btn = null;
            }
            else
            {
                _adornerLayer = adornerLayer;
                _btn = adornedElement;

                // adjust position if sizes change
                _adornerLayer.SizeChanged += (s, e) => { UpdatePosition(); };
                _btn.SizeChanged += (s, e) => { UpdatePosition(); };

                _contentPresenter = new ContentPresenter() { Content = data, ContentTemplate = dataTemplate };

                // apply template explicitly: http://stackoverflow.com/questions/5679648/why-would-this-contenttemplate-findname-throw-an-invalidoperationexception-on
                _contentPresenter.ApplyTemplate();

                // get close button from datatemplate
                Button closeBtn = _contentPresenter.ContentTemplate.FindName("PART_CloseButton", _contentPresenter) as Button;
                if (closeBtn != null)
                    closeBtn.Click += (s, e) => { this.Hide(); _btn = null; };

                _visualChildren = new VisualCollection(this); // this is needed for user interaction with the adorner layer
                _visualChildren.Add(_contentPresenter);

                _adornerLayer.Add(this);

                _currentInstance = this;

                UpdatePosition(); // position adorner
            }
        }


        /// <summary>
        /// Positioning is a bit fiddly. 
        /// Also, this method is only dealing with the right clip, not yet with the bottom clip.
        /// </summary>
        private void UpdatePosition()
        {
            double marginLeft = 0;
            _contentPresenter.Margin = new Thickness(marginLeft, 0, _marginRight, 0); // "reset" margin to get a good measure pass
            _contentPresenter.Measure(_adornerLayer.RenderSize); // measure the contentpresenter to get a DesiredSize
            var contentRect = new Rect(_contentPresenter.DesiredSize);
            double right = _btn.TranslatePoint(new Point(contentRect.Width, 0), _adornerLayer).X; // this does not work with the contentpresenter, so use _adornedElement

            if (right > _adornerLayer.ActualWidth) // if adorner is clipped by right window border, move it to the left
                marginLeft = _adornerLayer.ActualWidth - right;

            _contentPresenter.Margin = new Thickness(marginLeft, _btn.ActualHeight + _adornerDistance, _marginRight, 0); // position adorner

            DrawArrow();
        }

        private void DrawArrow()
        {
            Point bottomMiddleButton = new Point(_btn.ActualWidth / 2, _btn.ActualHeight - _btn.Margin.Bottom);
            Point topLeftAdorner = new Point(_btn.ActualWidth / 2 - 10, _contentPresenter.Margin.Top);
            Point topRightAdorner = new Point(_btn.ActualWidth / 2 + 10, _contentPresenter.Margin.Top);

            PointCollection points = new PointCollection();
            points.Add(bottomMiddleButton);
            points.Add(topLeftAdorner);
            points.Add(topRightAdorner);

            _points = points; // actual drawing executed in OnRender
        }

        protected override void OnRender(DrawingContext drawingContext)
        {
            // Drawing the arrow
            StreamGeometry streamGeometry = new StreamGeometry();
            using (StreamGeometryContext geometryContext = streamGeometry.Open())
            {
                if (_points != null && _points.Any())
                {
                    geometryContext.BeginFigure(_points[0], true, true);
                    geometryContext.PolyLineTo(_points.Where(p => _points.IndexOf(p) > 0).ToList(), true, true);
                }
            }

            // Draw the polygon visual
            drawingContext.DrawGeometry(Brushes.DarkOrchid, new Pen(_btn.Background, 0.5), streamGeometry);

            base.OnRender(drawingContext);
        }

        private void Hide()
        {
            _adornerLayer.Remove(this);
        }

        protected override Size MeasureOverride(Size constraint)
        {
            _contentPresenter.Measure(constraint);
            return _contentPresenter.DesiredSize;
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            _contentPresenter.Arrange(new Rect(finalSize));
            return finalSize;
        }

        protected override Visual GetVisualChild(int index)
        {
            return _visualChildren[index];
        }

        protected override int VisualChildrenCount
        {
            get { return _visualChildren.Count; }
        }
    }

    // http://stackoverflow.com/questions/397556/how-to-bind-radiobuttons-to-an-enum
    public class EnumToBooleanConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return value.Equals(parameter);
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return value.Equals(true) ? parameter : Binding.DoNothing;
        }
    }
}

ViewModel, MyContact:

视图模型,我的联系人:

using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Input;
namespace WpfApplication1
{
    public class ViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        private ObservableCollection<MyContact> _myContacts = new ObservableCollection<MyContact>();
        public ObservableCollection<MyContact> MyContacts { get { return _myContacts; } set { _myContacts = value; OnPropertyChanged("MyContacts"); } }


        public ViewModel()
        {
            MyContacts = new ObservableCollection<MyContact>()
            {
                new MyContact() { FullName = "Sigmund Freud", Gender = Gender.Male },
                new MyContact() { FullName = "Abraham Lincoln", Gender = Gender.Male },
                new MyContact() { FullName = "Joan Of Arc", Gender = Gender.Female },
                new MyContact() { FullName = "Bob the Khann", Gender = Gender.Male, Address = "Mongolia" },
                new MyContact() { FullName = "Freddy Mercury", Gender = Gender.Male },
                new MyContact() { FullName = "Giordano Bruno", Gender = Gender.Male },
                new MyContact() { FullName = "Socrates", Gender = Gender.Male },
                new MyContact() { FullName = "Marie Curie", Gender = Gender.Female }
            };
        }
    }

    public class MyContact : INotifyPropertyChanged
    {
        private string _fullName;
        public string FullName { get { return _fullName; } set { _fullName = value; OnPropertyChanged("FullName"); } }

        private string _address;
        public string Address { get { return _address; } set { _address = value; OnPropertyChanged("Address"); } }

        private Gender _gender;
        public Gender Gender { get { return _gender; } set { _gender = value; OnPropertyChanged("Gender"); } }

        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public enum Gender
    {
        Male,
        Female
    }

回答by Rachel

Personally I hate WPF's built in Popupcontrol for exactly those reasons, and my workaround is to use a Custom Popup UserControl

我个人讨厌 WPF 的内置Popup控件正是出于这些原因,我的解决方法是使用自定义弹出用户控件

Basically I'll put the Popup in a panel that allows it's children to overlap, such as a Gridor a Canvas, and position it on top of whatever content it's supposed to be on top of.

基本上,我会将 Popup 放在一个面板中,允许它的子项重叠,例如 aGrid或 a Canvas,并将其放置在它应该位于的任何内容之上。

It includes DependencyProperties to specify it's parent panel and if it's open or not, and is part of the normal VisualTree so it will move around with your Window and act the same way any regular UI element would.

它包括 DependencyProperties 以指定它的父面板以及它是否打开,并且是普通 VisualTree 的一部分,因此它将随您的窗口移动并以与任何常规 UI 元素相同的方式运行。

Typical usage would look like this:

典型用法如下所示:

<Grid x:Name="ParentPanel">
    <ItemsControl ... />

    <local:PopupPanel Content="{Binding PopupContent}"
        local:PopupPanel.PopupParent="{Binding ElementName=ParentPanel}"
        local:PopupPanel.IsPopupVisible="{Binding IsPopupVisible}" />
</Grid>

The code for the UserControl can be found on my blogalong with a downloadable example of its use, but I'll also post a copy of it here.

可以在我的博客上找到 UserControl 的代码及其使用的可下载示例,但我也会在此处发布它的副本。

The XAML for the UserControl is:

UserControl 的 XAML 是:

<UserControl x:Class="PopupPanelSample.PopupPanel"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:local="clr-namespace:PopupPanelSample"
             FocusManager.IsFocusScope="True"
             >

    <UserControl.Template>
        <ControlTemplate TargetType="{x:Type local:PopupPanel}">
            <ControlTemplate.Resources>
                <!-- Converter to get Popup Positioning -->
                <local:ValueDividedByParameterConverter x:Key="ValueDividedByParameterConverter" />

                <!-- Popup Visibility -->
                <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
                <Style x:Key="PopupPanelContentStyle" TargetType="{x:Type Grid}">
                    <Setter Property="Grid.Visibility" Value="{Binding Path=IsPopupVisible,
                        RelativeSource={RelativeSource AncestorType={x:Type local:PopupPanel}},
                        Converter={StaticResource BooleanToVisibilityConverter}}"/>
                </Style>
            </ControlTemplate.Resources>

            <Grid x:Name="PopupPanelContent" Style="{StaticResource PopupPanelContentStyle}">
                <Grid.Resources>
                    <!-- Storyboard to show Content -->
                    <Storyboard x:Key="ShowEditPanelStoryboard" SpeedRatio="5">
                        <DoubleAnimation
                            Storyboard.TargetName="PopupPanelContent"
                            Storyboard.TargetProperty="RenderTransform.(ScaleTransform.ScaleX)"
                            From="0.00" To="1.00" Duration="00:00:01"
                            />
                        <DoubleAnimation
                            Storyboard.TargetName="PopupPanelContent"
                            Storyboard.TargetProperty="RenderTransform.(ScaleTransform.ScaleY)"
                            From="0.00" To="1.00" Duration="00:00:01"
                            />
                    </Storyboard>
                </Grid.Resources>

                <!-- Setting up RenderTransform for Popup Animation -->
                <Grid.RenderTransform>
                    <ScaleTransform
                        CenterX="{Binding Path=PopupParent.ActualWidth, Converter={StaticResource ValueDividedByParameterConverter}, ConverterParameter=2, RelativeSource={RelativeSource AncestorType={x:Type local:PopupPanel}}}"
                        CenterY="{Binding Path=PopupParent.ActualHeight, Converter={StaticResource ValueDividedByParameterConverter}, ConverterParameter=2, RelativeSource={RelativeSource AncestorType={x:Type local:PopupPanel}}}"
                        />
                </Grid.RenderTransform>

                <!-- Grayscale background & prevents mouse input -->
                <Rectangle
                    Fill="Gray"
                    Opacity="{Binding Path=BackgroundOpacity, RelativeSource={RelativeSource AncestorType={x:Type local:PopupPanel}}}"
                    Height="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:PopupPanel}}, Path=Height}"
                    Width="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:PopupPanel}}, Path=Width}"
                    />

                <!-- Popup Content -->
                <ContentControl x:Name="PopupContentControl"
                                KeyboardNavigation.TabNavigation="Cycle"
                                PreviewKeyDown="PopupPanel_PreviewKeyDown"
                                PreviewLostKeyboardFocus="PopupPanel_LostFocus"
                                IsVisibleChanged="PopupPanel_IsVisibleChanged"
                                HorizontalAlignment="Center" VerticalAlignment="Center"
                                >
                    <ContentPresenter Content="{TemplateBinding Content}" />
                </ContentControl>
            </Grid>
        </ControlTemplate>
    </UserControl.Template>
</UserControl>

And the code-behind the UserControl looks like this:

UserControl 背后的代码如下所示:

using System;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Threading;

namespace PopupPanelSample
{
    /// <summary>
    /// Panel for handling Popups:
    /// - Control with name PART_DefaultFocusControl will have default focus
    /// - Can define PopupParent to determine if this popup should be hosted in a parent panel or not
    /// - Can define the property EnterKeyCommand to specifify what command to run when the Enter key is pressed
    /// - Can define the property EscapeKeyCommand to specify what command to run when the Escape key is pressed
    /// - Can define BackgroundOpacity to specify how opaque the background will be. Value is between 0 and 1.
    /// </summary>
    public partial class PopupPanel : UserControl
    {
        #region Fields

        bool _isLoading = false;                    // Flag to tell identify when DataContext changes
        private UIElement _lastFocusControl;        // Last control that had focus when popup visibility changes, but isn't closed

        #endregion // Fields

        #region Constructors

        public PopupPanel()
        {
            InitializeComponent();
            this.DataContextChanged += Popup_DataContextChanged;

            // Register a PropertyChanged event on IsPopupVisible
            DependencyPropertyDescriptor dpd = DependencyPropertyDescriptor.FromProperty(PopupPanel.IsPopupVisibleProperty, typeof(PopupPanel));
            if (dpd != null) dpd.AddValueChanged(this, delegate { IsPopupVisible_Changed(); });

            dpd = DependencyPropertyDescriptor.FromProperty(PopupPanel.ContentProperty, typeof(PopupPanel));
            if (dpd != null) dpd.AddValueChanged(this, delegate { Content_Changed(); });

        }

        #endregion // Constructors

        #region Events

        #region Property Change Events

        // When DataContext changes
        private void Popup_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            DisableAnimationWhileLoading();
        }

        // When Content Property changes
        private void Content_Changed()
        {
            DisableAnimationWhileLoading();
        }

        // Sets an IsLoading flag so storyboard doesn't run while loading
        private void DisableAnimationWhileLoading()
        {
            _isLoading = true;
            this.Dispatcher.BeginInvoke(DispatcherPriority.Render,
                new Action(delegate() { _isLoading = false; }));
        }

        // Run storyboard when IsPopupVisible property changes to true
        private void IsPopupVisible_Changed()
        {
            bool isShown = GetIsPopupVisible(this);

            if (isShown && !_isLoading)
            {
                FrameworkElement panel = FindChild<FrameworkElement>(this, "PopupPanelContent");
                if (panel != null)
                {
                    // Run Storyboard
                    Storyboard animation = (Storyboard)panel.FindResource("ShowEditPanelStoryboard");
                    animation.Begin();
                }
            }

            // When hiding popup, clear the LastFocusControl
            if (!isShown)
            {
                _lastFocusControl = null;
            }
        }

        #endregion // Change Events

        #region Popup Events

        // When visibility is changed, set the default focus
        void PopupPanel_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            if ((bool)e.NewValue)
            {
                ContentControl popupControl = FindChild<ContentControl>(this, "PopupContentControl");
                this.Dispatcher.BeginInvoke(DispatcherPriority.Render,
                    new Action(delegate()
                    {
                        // Verify object really is visible because sometimes it's not once we switch to Render
                        if (!GetIsPopupVisible(this))
                        {
                            return;
                        }

                        if (_lastFocusControl != null && _lastFocusControl.Focusable)
                        {
                            _lastFocusControl.Focus();
                        }
                        else
                        {
                            _lastFocusControl = FindChild<UIElement>(popupControl, "PART_DefaultFocusControl") as UIElement;

                            // If we can find the part named PART_DefaultFocusControl, set focus to it
                            if (_lastFocusControl != null && _lastFocusControl.Focusable)
                            {
                                _lastFocusControl.Focus();
                            }
                            else
                            {
                                _lastFocusControl = FindFirstFocusableChild(popupControl);

                                // If no DefaultFocusControl found, try and set focus to the first focusable element found in popup
                                if (_lastFocusControl != null)
                                {
                                    _lastFocusControl.Focus();
                                }
                                else
                                {
                                    // Just give the Popup UserControl focus so it can handle keyboard input
                                    popupControl.Focus();
                                }
                            }
                        }
                    }
                    )
                );
            }
        }

        // When popup loses focus but isn't hidden, store the last element that had focus so we can put it back later
        void PopupPanel_LostFocus(object sender, RoutedEventArgs e)
        {
            DependencyObject focusScope = FocusManager.GetFocusScope(this);
            _lastFocusControl = FocusManager.GetFocusedElement(focusScope) as UIElement;
        }

        // Keyboard Events
        private void PopupPanel_PreviewKeyDown(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Escape)
            {
                PopupPanel popup = FindAncester<PopupPanel>((DependencyObject)sender);
                ICommand cmd = GetPopupEscapeKeyCommand(popup);
                if (cmd != null && cmd.CanExecute(null))
                {
                    cmd.Execute(null);
                    e.Handled = true;
                }
                else
                {
                    // By default the Escape Key closes the popup when pressed
                    var expression = this.GetBindingExpression(PopupPanel.IsPopupVisibleProperty);
                    var dataType = expression.DataItem.GetType();
                    dataType.GetProperties().Single(x => x.Name == expression.ParentBinding.Path.Path)
                        .SetValue(expression.DataItem, false, null);
                }
            }
            else if (e.Key == Key.Enter)
            {
                // Don't want to run Enter command if focus is in a TextBox with AcceptsReturn = True
                if (!(e.KeyboardDevice.FocusedElement is TextBox &&
                     (e.KeyboardDevice.FocusedElement as TextBox).AcceptsReturn == true))
                {
                    PopupPanel popup = FindAncester<PopupPanel>((DependencyObject)sender);
                    ICommand cmd = GetPopupEnterKeyCommand(popup);
                    if (cmd != null && cmd.CanExecute(null))
                    {
                        cmd.Execute(null);
                        e.Handled = true;
                    }
                }

            }
        }

        #endregion // Popup Events

        #endregion // Events

        #region Dependency Properties

        // Parent for Popup
        #region PopupParent

        public static readonly DependencyProperty PopupParentProperty =
            DependencyProperty.Register("PopupParent", typeof(FrameworkElement),
            typeof(PopupPanel), new PropertyMetadata(null, null, CoercePopupParent));

        private static object CoercePopupParent(DependencyObject obj, object value)
        {
            // If PopupParent is null, return the Window object
            return (value ?? FindAncester<Window>(obj));
        }

        public FrameworkElement PopupParent
        {
            get { return (FrameworkElement)this.GetValue(PopupParentProperty); }
            set { this.SetValue(PopupParentProperty, value); }
        }

        // Providing Get/Set methods makes them show up in the XAML designer
        public static FrameworkElement GetPopupParent(DependencyObject obj)
        {
            return (FrameworkElement)obj.GetValue(PopupParentProperty);
        }

        public static void SetPopupParent(DependencyObject obj, FrameworkElement value)
        {
            obj.SetValue(PopupParentProperty, value);
        }

        #endregion

        // Popup Visibility - If popup is shown or not
        #region IsPopupVisibleProperty

        public static readonly DependencyProperty IsPopupVisibleProperty =
            DependencyProperty.Register("IsPopupVisible", typeof(bool),
            typeof(PopupPanel), new PropertyMetadata(false, null));

        public static bool GetIsPopupVisible(DependencyObject obj)
        {
            return (bool)obj.GetValue(IsPopupVisibleProperty);
        }

        public static void SetIsPopupVisible(DependencyObject obj, bool value)
        {
            obj.SetValue(IsPopupVisibleProperty, value);
        }

        #endregion // IsPopupVisibleProperty

        // Transparency level for the background filler outside the popup
        #region BackgroundOpacityProperty

        public static readonly DependencyProperty BackgroundOpacityProperty =
            DependencyProperty.Register("BackgroundOpacity", typeof(double),
            typeof(PopupPanel), new PropertyMetadata(.5, null));

        public static double GetBackgroundOpacity(DependencyObject obj)
        {
            return (double)obj.GetValue(BackgroundOpacityProperty);
        }

        public static void SetBackgroundOpacity(DependencyObject obj, double value)
        {
            obj.SetValue(BackgroundOpacityProperty, value);
        }

        #endregion ShowBackgroundProperty

        // Command to execute when Enter key is pressed
        #region PopupEnterKeyCommandProperty

        public static readonly DependencyProperty PopupEnterKeyCommandProperty =
            DependencyProperty.RegisterAttached("PopupEnterKeyCommand", typeof(ICommand),
            typeof(PopupPanel), new PropertyMetadata(null, null));

        public static ICommand GetPopupEnterKeyCommand(DependencyObject obj)
        {
            return (ICommand)obj.GetValue(PopupEnterKeyCommandProperty);
        }

        public static void SetPopupEnterKeyCommand(DependencyObject obj, ICommand value)
        {
            obj.SetValue(PopupEnterKeyCommandProperty, value);
        }

        #endregion PopupEnterKeyCommandProperty

        // Command to execute when Enter key is pressed
        #region PopupEscapeKeyCommandProperty

        public static readonly DependencyProperty PopupEscapeKeyCommandProperty =
            DependencyProperty.RegisterAttached("PopupEscapeKeyCommand", typeof(ICommand),
            typeof(PopupPanel), new PropertyMetadata(null, null));

        public static ICommand GetPopupEscapeKeyCommand(DependencyObject obj)
        {
            return (ICommand)obj.GetValue(PopupEscapeKeyCommandProperty);
        }

        public static void SetPopupEscapeKeyCommand(DependencyObject obj, ICommand value)
        {
            obj.SetValue(PopupEscapeKeyCommandProperty, value);
        }

        #endregion PopupEscapeKeyCommandProperty

        #endregion Dependency Properties

        #region Visual Tree Helpers

        public static UIElement FindFirstFocusableChild(DependencyObject parent)
        {
            // Confirm parent is valid.
            if (parent == null) return null;

            UIElement foundChild = null;

            int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
            for (int i = 0; i < childrenCount; i++)
            {
                UIElement child = VisualTreeHelper.GetChild(parent, i) as UIElement;

                // This is returning me things like ContentControls, so for now filtering to buttons/textboxes only
                if (child != null && child.Focusable && child.IsVisible)
                {
                    foundChild = child;
                    break;
                }
                // recursively drill down the tree
                foundChild = FindFirstFocusableChild(child);

                // If the child is found, break so we do not overwrite the found child.
                if (foundChild != null) break;
            }
            return foundChild;
        }

        public static T FindAncester<T>(DependencyObject current)
        where T : DependencyObject
        {
            // Need this call to avoid returning current object if it is the same type as parent we are looking for
            current = VisualTreeHelper.GetParent(current);

            while (current != null)
            {
                if (current is T)
                {
                    return (T)current;
                }
                current = VisualTreeHelper.GetParent(current);
            };
            return null;
        }

        /// <summary>
        /// Looks for a child control within a parent by name
        /// </summary>
        public static T FindChild<T>(DependencyObject parent, string childName)
        where T : DependencyObject
        {
            // Confirm parent and childName are valid.
            if (parent == null) return null;

            T foundChild = null;

            int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
            for (int i = 0; i < childrenCount; i++)
            {
                var child = VisualTreeHelper.GetChild(parent, i);
                // If the child is not of the request child type child
                T childType = child as T;
                if (childType == null)
                {
                    // recursively drill down the tree
                    foundChild = FindChild<T>(child, childName);

                    // If the child is found, break so we do not overwrite the found child.
                    if (foundChild != null) break;
                }
                else if (!string.IsNullOrEmpty(childName))
                {
                    var frameworkElement = child as FrameworkElement;
                    // If the child's name is set for search
                    if (frameworkElement != null && frameworkElement.Name == childName)
                    {
                        // if the child's name is of the request name
                        foundChild = (T)child;
                        break;
                    }
                    else
                    {
                        // recursively drill down the tree
                        foundChild = FindChild<T>(child, childName);

                        // If the child is found, break so we do not overwrite the found child.
                        if (foundChild != null) break;
                    }
                }
                else
                {
                    // child element found.
                    foundChild = (T)child;
                    break;
                }
            }

            return foundChild;
        }

        #endregion

    }

    // Converter for Popup positioning
    public class ValueDividedByParameterConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            double n, d;
            if (double.TryParse(value.ToString(), out n)
                && double.TryParse(parameter.ToString(), out d)
                && d != 0)
            {
                return n / d;
            }

            return 0;
        }        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}