单击以编辑控件 LostFocus 事件问题

Click-to-Edit Control LostFocus event issue

我正在开发一个简单的自定义控件,双击它应该进入编辑模式

这个概念是基于这个问题Click-to-edit in Silverlight

双击它会更改编辑模板上的初始模板 它似乎很清楚,除了部分(5)当控件失去焦点时如何更改模板 仅当包含的控件失去焦点时才会触发 Lost Focus 事件 这是一篇谈论它的文章 http://programmerpayback.com/2008/11/20/gotfocus-and-lostfocus-events-on-containers/

我已经尝试实施相同的技术,但仍然没有结果,当我在控件外部单击时,我无法让 LostFocus 事件为我工作

我的问题在哪里?

我的XAML

<ContentControl x:Class="Splan_RiaBusinessApplication.Controls.TimeCodeControl"
    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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:obj="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
    xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"  
    xmlns:behaviour="clr-namespace:Splan_RiaBusinessApplication.Behavior"
    xmlns:controls="clr-namespace:Splan_RiaBusinessApplication.Controls"
    xmlns:Primitives="clr-namespace:System.Windows.Controls.Primitives;assembly=System.Windows.Controls.Data"
    mc:Ignorable="d"
    IsTabStop="True"
    IsEnabled="True"
    Visibility="Visible"
    d:DesignHeight="100" d:DesignWidth="200"
    d:Height="200" d:Width="200"
                >
    <ContentControl.Resources>
        <ControlTemplate x:Key="DisplayTemplate">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition />
                    <RowDefinition />
                </Grid.RowDefinitions>
                <TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" Text="{Binding Target.Code, Mode=TwoWay}" />
                <StackPanel Grid.Row="1" Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center" >
                    <TextBlock Text="{Binding Target.Start, Mode=TwoWay, StringFormat=hh\:mm }" />
                    <TextBlock Text='-' />
                    <TextBlock Text="{Binding Target.End, Mode=TwoWay, StringFormat=hh\:mm }" />
                </StackPanel>
            </Grid>
        </ControlTemplate>
        
        <ControlTemplate x:Key="EditTemplate">
            <Grid Background="Aqua" Height="200" Width="200">
                <Grid.RowDefinitions>
                    <RowDefinition />
                    <RowDefinition />
                </Grid.RowDefinitions>
                <ComboBox Width="100" Height="25" x:Name="cbTimeCode"
                        ItemsSource="{Binding TimeCodes}"
                        SelectedValue="{Binding Target.CodeId, Mode=TwoWay}"                                                                                           
                        SelectedValuePath="TimeCodeId"
                    >
                    <ComboBox.ItemTemplate>
                        <DataTemplate>
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="40"/>
                                    <ColumnDefinition Width="100"/>
                                </Grid.ColumnDefinitions>
                                <TextBlock Text="{Binding Target.Code}" />
                                <TextBlock Grid.Column="1" Text="{Binding Target.Description}" />
                            </Grid>
                        </DataTemplate>
                    </ComboBox.ItemTemplate>
                    <i:Interaction.Triggers>
                        <i:EventTrigger>
                            <behaviour:ResolveElementName PropertyName="ItemsSource" />
                        </i:EventTrigger>
                    </i:Interaction.Triggers>
                </ComboBox>

                <!--<controls:TimeRangePickerControl Grid.Row="1" StartTime="{Binding Target.Start, Mode=TwoWay}"  EndTime="{Binding Target.End, Mode=TwoWay}"/>-->
            </Grid>
        </ControlTemplate>
    </ContentControl.Resources>
    
    <Grid x:Name="Layout" Background="Aquamarine">
        <ItemsControl x:Name="PlaceHolder" Template="{StaticResource DisplayTemplate}">
        </ItemsControl>
    </Grid>
</ContentControl>

代码隐藏

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;


namespace Splan_RiaBusinessApplication.Controls
{
public class TimeCode
{

    public int TimeCodeId {get;set;}

    public string Code { get; set; }

    public string Description { get; set; }
}

public class TimeDetail
{
    public int TimeDetailId { get;set; }

    public int CodeId { get;set;}

    public TimeSpan Start { get; set; }
    public TimeSpan End { get; set; }

    public string Code { get; set; }

    public string Comment { get; set; }
}

    public partial class TimeCodeControl : ContentControl
    {

        public class TimeCodeControlEventArgs : EventArgs
        {
            public string userName { get; set; }
        }

        private static TimeSpan DoubleClickThreshold = TimeSpan.FromMilliseconds(300);
        private DateTime _lastClick;

        private Boolean m_EditMode = false;
        public Boolean EditMode
        {
            get { return m_EditMode; }
            set
            {
                if (m_EditMode != value)
                {
                    switch (value)
                    {
                        case false:
                            PlaceHolder.Template = this.Resources["DisplayTemplate"] as ControlTemplate;                            
                            break;

                        case true:
                            PlaceHolder.Template = this.Resources["EditTemplate"] as ControlTemplate;
                            break;
                    }

                    m_EditMode = value;
                }
            }
        }

        public bool IsFocused
        {
            get
            {
                return FocusManager.GetFocusedElement() == this;
            }
        }


        public TimeCodeControl()
        {

            TimeCodes = new List<TimeCode>() { new TimeCode { TimeCodeId = 200, Code= "C", Description="Cash" } };
            InitializeComponent();

            Layout.DataContext = this;
            this.IsTabStop = true;
            this.Visibility = Visibility.Visible;
            this.IsEnabled = true;
            this.Focus();


            Layout.MouseLeftButtonDown += Layout_MouseLeftButtonDown;
            //Layout.KeyDown += Layout_KeyDown;
            //Layout.KeyUp += Layout_KeyUp;
            this.LostFocus += TimeCodeControl_LostFocus;
            this.GotFocus += TimeCodeControl_GotFocus;

        }


        void TimeCodeControl_GotFocus(object sender, RoutedEventArgs e)
        {
        }

        void TimeCodeControl_LostFocus(object sender, RoutedEventArgs e)
        {
        }

        public TimeDetail Source
        {
            get { return (TimeDetail)GetValue(SourceProperty); }
            set { SetValue(SourceProperty, value); }
        }

        public static readonly DependencyProperty SourceProperty =
            DependencyProperty.Register("Source", typeof(TimeDetail), typeof(TimeCodeControl),
                                        new PropertyMetadata(SourceChanged));

        private static void SourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            var control = sender as TimeCodeControl;
            if (control == null) return;

            control.Target = control.Source; //.Copy();

        }

        public List<TimeCode> TimeCodes { get; set; }

        public TimeDetail Target { get; set; }


        private bool FocusIsInside(object parent)
        {
            bool rs = false;
            dynamic oFocus = FocusManager.GetFocusedElement();
            while (oFocus != null)
                try
                {
                    if ((oFocus.GetType() == parent.GetType()) && (oFocus.Equals(this)))
                    {
                        rs = true;
                        break;
                    }
                    else
                    {
                        oFocus = oFocus.Parent;
                    }
                }
                catch
                {
                    break;
                }

            return rs;
        }

        private Boolean hasFocus = false;

        protected override void OnGotFocus(RoutedEventArgs e)
        {
            base.OnGotFocus(e);
            if (!hasFocus)
            {
                hasFocus = true;
                Debug.WriteLine("Container Got Focus");
            }
        }
        protected override void OnLostFocus(RoutedEventArgs e)
        {
            base.OnLostFocus(e);

            //if (!FocusIsInside(Layout))
            //{
            //    hasFocus = false;
            //    Debug.WriteLine("Container Lost Focus");
            //    EditMode = false;
            //}

        }


        void Layout_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            if (DateTime.Now - this._lastClick <= DoubleClickThreshold)
            {
                EditMode = true;
                this._lastClick = DateTime.Now;

                e.Handled = true;
                return;
            }
            this._lastClick = DateTime.Now;

        }
    }
}

更新: 我决定使用计时器来识别用户从容器外部带来焦点或只是将焦点从一个控件切换到容器内部的另一个控件时的场景。它可能不是最好的解决方案,但它现在似乎正在工作。对于不同方法或实施的任何建议或建议,我将不胜感激。

public partial class    MyControl: ContentControl
{
   ...

   public event EventHandler<RoutedEventArgs> LostFocus;
   public event EventHandler<RoutedEventArgs> GotFocus;

   bool Focused = false;
   DispatcherTimer FocusTimer = null;

    protected override void OnGotFocus(RoutedEventArgs e)
    {
        base.OnGotFocus(e);
        if (Focused) return;

        Focused = true;

        // it focused from the outside of the control
        // becouse the timer wasn't initialised on the previous LostFocused event
        // generated by other control in the same ContentControl contaner
        if (FocusTimer == null)
        {
           if (GotFocus != null)
                GotFocus(e.OriginalSource, e);

            Debug.WriteLine("Got Focus ");

            return;
        }

        // It was switched from one hosted control to another one
        FocusTimer.Stop();

        FocusTimer = null;
    }

        protected override void OnLostFocus(RoutedEventArgs e)
        {
            base.OnLostFocus(e);

        if (e.OriginalSource is ComboBox && FocusManager.GetFocusedElement() is ComboBoxItem)
            return;

            FocusTimer = new DispatcherTimer();

            Focused = false;

            FocusTimer.Interval = new TimeSpan(0, 0, 0, 0, 50);
            FocusTimer.Tick += (s, args) =>
            {
                FocusTimer.Stop();
                FocusTimer = null;

                // if after the timeout the focus still not triggered 
                // by another contained element
                // the We lost a focus on the container
                if (!Focused )
                {
                    if(LostFocus != null)
                        LostFocus(e.OriginalSource, e);

                    Debug.WriteLine("Lost Focus " );
                }

            };

            FocusTimer.Start();
        }

   ...
}

有几个问题。让我们看看...

Why are you not getting a LostFocus event when you click outside of the control?

好吧,我前段时间也成了这个错误假设的受害者。外部点击不会改变焦点,除非您点击一个在点击时明确将焦点设置到自身的控件(就像 TextBox 或各种按钮一样)。 按 T​​ab 将键盘焦点导航到下一个控件并查看是否引发事件。

但是让我们谈谈其他问题:

ControlTemplate x:Key="DisplayTemplate" and ControlTemplate x:Key="EditTemplate"

不推荐使用ControlTemplates这种方式。而是使用 DataTemplate 和相应的 ContentPresenters.

TimeCodeControl : ContentControl and x:Class="Splan_RiaBusinessApplication.Controls.TimeCodeControl"

是的,我知道这是可能的,但不是很有用。让我解释: 您可以编写自己的专用点击编辑控件作为一次性工具:使用硬编码的 DisplayTemplate 和 EditTemplate 来编辑 TimeCodeTimeDetail 数据(基本上就是您所做的)。但是你没有机会使用它并指定另一对模板来允许编辑其他数据类型。 所以从ContentControl派生没有多大意义,你也可以从UserControl派生。

另一种方法是:将您的“点击编辑”控件编写为通用且可重复使用的控件,它提供两个 public 属性:DisplayTemplate 和 EditTemplate。并且不要对您的 DataContext 做出任何假设。同样,将 ContentControl 作为父级没有任何好处 class。 我建议你从 Control 派生,如前所述添加两个 DataTemplate 类型的 DependencyProperties,定义一个默认的 ControlTemplate,其中包含一个或两个 ContentPresenters。在您的控制代码中,您需要处理 MouseLeftButtonDown 和 LostFocus 并相应地更新布尔标志。

这是一个工作示例:

...确定焦点的扩展方法:

public static class ControlExtensions
{
    public static bool IsFocused( this UIElement control )
    {
        DependencyObject parent;
        for (DependencyObject potentialSubControl = FocusManager.GetFocusedElement() as DependencyObject; potentialSubControl != null; potentialSubControl = parent)
        {
            if (object.ReferenceEquals( potentialSubControl, control ))
            {
                return true;
            }
            parent = VisualTreeHelper.GetParent( potentialSubControl );
            if (parent == null)
            {
                FrameworkElement element = potentialSubControl as FrameworkElement;
                if (element != null)
                {
                    parent = element.Parent;
                }
            }
        }
        return false;
    }
}

...和一个不错的自定义控件:

public class ClickToEditControl : Control
{
    public ClickToEditControl()
    {
        DefaultStyleKey = typeof (ClickToEditControl);
        MouseLeftButtonDown += OnMouseLeftButtonDown;
    }

    private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        if (e.ClickCount==2)
        {
            GotoEditMode();
            e.Handled = true;
        }
    }

    protected override void OnLostFocus(RoutedEventArgs e)
    {
        base.OnLostFocus(e);

        if (!this.IsFocused())
            GotoDisplayMode();
    }

    private void GotoDisplayMode()
    {
        IsInEditMode = false;
    }

    private void GotoEditMode()
    {
        IsInEditMode = true;
    }

    public DataTemplate EditTemplate
    {
        get { return (DataTemplate) GetValue( EditTemplateProperty ); }
        set { SetValue( EditTemplateProperty, value ); }
    }

    public static readonly DependencyProperty EditTemplateProperty =
        DependencyProperty.Register( "EditTemplate", typeof( DataTemplate ), typeof( ClickToEditControl ), null );

    public DataTemplate DisplayTemplate
    {
        get { return (DataTemplate) GetValue( DisplayTemplateProperty ); }
        set { SetValue( DisplayTemplateProperty, value ); }
    }

    public static readonly DependencyProperty DisplayTemplateProperty =
        DependencyProperty.Register( "DisplayTemplate", typeof( DataTemplate ), typeof( ClickToEditControl ), null );

    public bool IsInEditMode
    {
        get { return (bool) GetValue( IsInEditModeProperty ); }
        set { SetValue( IsInEditModeProperty, value ); }
    }

    public static readonly DependencyProperty IsInEditModeProperty =
        DependencyProperty.Register( "IsInEditMode", typeof( bool ), typeof( ClickToEditControl ), null );
}

...和控制模板:

<clickToEdit:BoolToVisibilityConverter x:Key="VisibleIfInEditMode"/>
<clickToEdit:BoolToVisibilityConverter x:Key="CollapsedIfInEditMode" VisibleIfTrue="False"/>

<Style TargetType="clickToEdit:ClickToEditControl">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="clickToEdit:ClickToEditControl">
                <Grid>
                    <ContentPresenter
                        ContentTemplate="{TemplateBinding EditTemplate}"
                        Content="{Binding}"
                        Visibility="{Binding IsInEditMode, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource VisibleIfInEditMode}}"/>
                    <ContentPresenter
                        ContentTemplate="{TemplateBinding DisplayTemplate}"
                        Content="{Binding}"
                        Visibility="{Binding IsInEditMode, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource CollapsedIfInEditMode}}"/>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

BoolToVisibilityConverter

public class BoolToVisibilityConverter : IValueConverter
{
    public bool VisibleIfTrue { get; set; }

    public BoolToVisibilityConverter(){VisibleIfTrue = true;}

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (VisibleIfTrue)
            return ((bool) value) ? Visibility.Visible : Visibility.Collapsed;
        else
            return ((bool) value) ? Visibility.Collapsed : Visibility.Visible;
    }

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

用法:

<clickToEdit:ClickToEditControl Height="20" Width="200">
        <clickToEdit:ClickToEditControl.DisplayTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding MyText}"/>
            </DataTemplate>
        </clickToEdit:ClickToEditControl.DisplayTemplate>
        <clickToEdit:ClickToEditControl.EditTemplate>
            <DataTemplate>
                <TextBox Text="{Binding MyText, Mode=TwoWay}"/>
            </DataTemplate>
        </clickToEdit:ClickToEditControl.EditTemplate>
    </clickToEdit:ClickToEditControl>