Click-to-Edit控制LostFocus事件问题

时间:2015-02-19 17:54:52

标签: c# silverlight custom-controls

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

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

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

我试图实现相同的Technic但仍然没有结果,当我点击控件外部时,我无法让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();
        }

   ...
}

1 个答案:

答案 0 :(得分:2)

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

  

为什么在控件外部单击时没有收到LostFocus事件?

好吧,我前段时间也成了这个错误假设的受害者。除非您单击在单击时显式将焦点设置为自身的控件(如TextBox,或各种按钮),否则外部单击不会更改焦点。 按Tab键将键盘焦点导航到下一个控件,并查看是否引发了事件。

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

  

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

不建议以这种方式使用ControlTemplates。而是使用DataTemplate和相应的ContentPresenters

  

TimeCodeControl : ContentControlx:Class="Splan_RiaBusinessApplication.Controls.TimeCodeControl"

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

另一种方法是:将Click-To-Edit控件编写为通用且可重用的控件,它提供两个公共属性:DisplayTemplate和EditTemplate。并且不要对DataContext做任何假设。再次将ContentControl作为父类没有任何好处。 我建议您从Control派生,如前所述添加两个DependencyProperties类型DataTemplate,定义一个内置一个或两个ContentPresenters的默认ControlTemplate。在您的控制代码中,您需要处理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 );
}

...和ControlTemplate:

<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>