简体   繁体   中英

Click-to-Edit Control LostFocus event issue

I am working on a simple Custom Control that should go to Edit mode by double clicking on it

The concept is based on this question Click-to-edit in Silverlight

On a double click it changes initial template on Edit Template and it seems to be pretty clear, except the part (5) How to change the template Back when the Control Looses the focus The Lost Focus event is fired only when contained controls are loosing focus Here is an article that talk about it http://programmerpayback.com/2008/11/20/gotfocus-and-lostfocus-events-on-containers/

I have tried to implement same Technic but still no result, I cannot get LostFocus event working for me when I click outside of a control

Where is my issue?

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

Code Behind

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

UPDATE : I decided to utilize timers to identify a scenario when user brings a focus from outside of the container or just switches focus from one control to another inside of the container. it may be not the best solution but it seems to be working for now. I would appreciate any suggestions or recommendations on different approaches or implementations.

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

There are several issues. Let's see...

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

Well, I fell victim to this false assumption some time ago too. The click outside does not change the focus unless you click a control that explicitly sets focus to itself on click (like a TextBox does, or the various Buttons). Press Tab to navigate the keyboard focus to the next control and see if the event is raised.

But let's talk about the other issues:

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

Using ControlTemplates this way is not recommended. Rather use DataTemplate and corresponding ContentPresenters .

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

Yes I know that's possible, but not really useful. Let me explain: You can write your own specialized Click-To-Edit Control as a one-shot tool: having a hardcoded DisplayTemplate and EditTemplate to edit TimeCode and TimeDetail data (basically what you did). But then you have no chance of ever using it and specifying another pair of Templates to allow editing of other data types. So it doesn't make much sense to derive from ContentControl, you could as well derive from UserControl.

An alternative would be: Write your Click-To-Edit control as a general and reusable control, that offers two public properties: DisplayTemplate and EditTemplate. And don't make any assumptions about your DataContext. And again there is no benefit from having ContentControl as the parent class. I recommend you derive from Control , add two DependencyProperties of type DataTemplate as mentioned earlier, define a default ControlTemplate with one or two ContentPresenters inside. In your control code you need to handle MouseLeftButtonDown and LostFocus and update a boolean flag accordingly.

Here is a working example:

...extension method to determine focus:

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

...and a nice custom control:

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

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

Usage:

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM