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
Locking popup position to element, or faking a popup with layers for in-place editing in an ItemsControl
提问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) 的多个实例的水平对象,它显示为带有人名的粉红色小字形。看起来像这样


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
一个弹出对话框,我能够证明这个“名”(其他像绑定对象的事情性质的编辑器Address,Gender等等),这工作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
当我点击一个看起来像的名字时给出一个输出


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.ZIndex在PanelControl我的 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:
所做的就是这样:


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


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();
}
}
}

