Domanda

Ok, ho un controllo che ha una proprietà IsEditing che, per motivi di argomento, ha un modello predefinito che normalmente è un blocco di testo, ma quando IsEditing è true, viene scambiato in una casella di testo per la modifica sul posto.Ora, quando il controllo perde il focus, se è ancora in fase di modifica, dovrebbe uscire dalla modalità di modifica e tornare al modello TextBlock.Abbastanza semplice, vero?

Pensa al comportamento di rinominare un file in Esplora risorse o sul desktop (che è la stessa cosa che conosco...) Questo è il comportamento che vogliamo.

Il problema è che non puoi utilizzare l'evento LostFocus perché quando passi a un'altra finestra (o elemento che è un FocusManager) LostFocus non si attiva poiché il controllo ha ancora il focus logico, quindi non funzionerà.

Se invece usi LostKeyboardFocus, anche se questo risolve il problema dell'"altro FocusManager", ora ne hai uno nuovo:quando stai modificando e fai clic con il pulsante destro del mouse sulla casella di testo per mostrare il menu contestuale, poiché il menu contestuale ora ha il focus della tastiera, il tuo controllo perde il focus della tastiera, esce dalla modalità di modifica e chiude il menu contestuale, confondendo l'utente!

Ora ho provato a impostare un flag per ignorare LostKeyboardFocus appena prima dell'apertura del menu, quindi a utilizzare quel fiag nell'evento LostKeyboardFocus per determinare se espellerlo dalla modalità di modifica o meno, ma se il menu è aperto e faccio clic altrove nel app, poiché il controllo stesso non aveva più il focus della tastiera (il menu lo aveva) il controllo non riceve mai un altro evento LostKeyboardFocus, quindi rimane in modalità di modifica.(Potrei dover aggiungere un controllo quando il menu si chiude per vedere cosa è attivo, quindi espellerlo manualmente da EditMode se non è il controllo.Sembra promettente.)

COSÌ...qualcuno ha qualche idea di come posso codificare con successo questo comportamento?

Segno

È stato utile?

Soluzione

OK...questo era "divertente" come in Programmer-fun.È stato un vero rompicapo da capire, ma con un bel sorriso enorme sul viso che l'ho fatto.(È ora di prendere un po' di IcyHot per la mia spalla considerando che me la sto dando una pacca così forte!:P )

Ad ogni modo è una cosa in più passaggi ma è sorprendentemente semplice una volta capito tutto.La versione breve è quella che devi usare Entrambi LostFocus E LostKeyboardFocus, né l'uno né l'altro.

LostFocus è facile.Ogni volta che ricevi quell'evento, imposta IsEditing a falso.Fatto e fatto.

Menu contestuali e focus della tastiera perso

LostKeyboardFocus è un po' più complicato dal momento che il menu contestuale del tuo controllo può attivarlo sul controllo stesso (ad es.quando si apre il menu contestuale del tuo controllo, il controllo ha ancora il focus ma perde il focus della tastiera e quindi, LostKeyboardFocus incendi.)

Per gestire questo comportamento, esegui l'override ContextMenuOpening (o gestire l'evento) e impostare un flag a livello di classe che indica che il menu si sta aprendo.(Io uso bool _ContextMenuIsOpening.) Poi nel LostKeyboardFocus override (o evento), controlli quel flag e, se è impostato, lo cancelli semplicemente e non fai nient'altro.Se non è impostato, tuttavia, significa che qualcosa oltre all'apertura del menu contestuale sta causando la perdita del focus della tastiera da parte del controllo, quindi in quel caso vuoi impostare IsEditing a falso.

Menu contestuali già aperti

Ora si verifica uno strano comportamento per cui se il menu contestuale di un controllo è aperto e quindi il controllo ha già perso il focus della tastiera come descritto sopra, se fai clic altrove nell'applicazione, prima che il nuovo controllo venga attivato, il tuo controllo verrà attivato per primo dalla tastiera , ma solo per una frazione di secondo, poi lo cede immediatamente al nuovo controllo.

Questo in realtà funziona a nostro vantaggio perché significa che ne otterremo anche un altro LostKeyboardFocus evento ma questa volta il flag _ContextMenuOpening sarà impostato su false e, proprio come descritto sopra, our LostKeyboardFocus il gestore verrà quindi impostato IsEditing a false, che è esattamente ciò che vogliamo.Adoro la serendipità!

Ora se il focus fosse semplicemente spostato sul controllo su cui hai fatto clic senza prima riportare il focus sul controllo che possiede il menu contestuale, allora dovremmo fare qualcosa come agganciare il ContextMenuClosing evento e controllando quale controllo verrà messo a fuoco successivamente, quindi imposteremo solo IsEditing a false se il controllo che verrà presto focalizzato non era quello che ha generato il menu contestuale, quindi abbiamo praticamente schivato un punto elenco lì.

Avvertimento:Menu contestuali predefiniti

Ora c'è anche l'avvertenza che se stai usando qualcosa come una casella di testo e non hai impostato esplicitamente il tuo menu contestuale su di essa, allora tu non ottenere il ContextMenuOpening evento, che mi ha sorpreso.Tuttavia, questo problema è facilmente risolvibile, semplicemente creando un nuovo menu contestuale con gli stessi comandi standard del menu contestuale predefinito (ad es.taglia, copia, incolla, ecc.) e assegnandolo alla casella di testo.Sembra esattamente lo stesso, ma ora ottieni l'evento di cui hai bisogno per impostare la bandiera.

Tuttavia, anche in questo caso hai un problema come se stai creando un controllo riutilizzabile di terze parti e l'utente di quel controllo desidera avere il proprio menu contestuale, potresti accidentalmente impostare il tuo su una precedenza più alta e sovrascrivere il loro !

Il modo per aggirare il problema è stato dal momento che la casella di testo è in realtà un elemento nel file IsEditing modello per il mio controllo, ho semplicemente aggiunto un nuovo DP sul controllo esterno chiamato IsEditingContextMenu che poi lego alla casella di testo tramite un internal TextBox style, quindi ho aggiunto a DataTrigger in quello stile che controlla il valore di IsEditingContextMenu sul controllo esterno e se è nullo, imposto il menu predefinito che ho appena creato sopra, che è archiviato in una risorsa.

Ecco lo stile interno per la casella di testo (l'elemento denominato "Root" rappresenta il controllo esterno che l'utente inserisce effettivamente nel proprio XAML)...

<Style x:Key="InlineTextbox" TargetType="TextBox">

    <Setter Property="OverridesDefaultStyle" Value="True"/>
    <Setter Property="FocusVisualStyle"      Value="{x:Null}" />
    <Setter Property="ContextMenu"           Value="{Binding IsEditingContextMenu, ElementName=Root}" />

    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type TextBoxBase}">

                <Border Background="White" BorderBrush="LightGray" BorderThickness="1" CornerRadius="1">
                    <ScrollViewer x:Name="PART_ContentHost" />
                </Border>

            </ControlTemplate>
        </Setter.Value>
    </Setter>

    <Style.Triggers>
        <DataTrigger Binding="{Binding IsEditingContextMenu, RelativeSource={RelativeSource AncestorType=local:EditableTextBlock}}" Value="{x:Null}">
            <Setter Property="ContextMenu">
                <Setter.Value>
                    <ContextMenu>
                        <MenuItem Command="ApplicationCommands.Cut" />
                        <MenuItem Command="ApplicationCommands.Copy" />
                        <MenuItem Command="ApplicationCommands.Paste" />
                    </ContextMenu>
                </Setter.Value>
            </Setter>
        </DataTrigger>
    </Style.Triggers>

</Style>

Tieni presente che devi impostare l'associazione del menu contestuale iniziale nello stile, non direttamente nella casella di testo, altrimenti il ​​DataTrigger dello stile viene sostituito dal valore impostato direttamente rendendo il trigger inutilizzabile e sei tornato al punto di partenza se la persona usa 'null' per il menu contestuale.(Se VUOI sopprimere il menu, non utilizzeresti comunque 'null'.Lo imposteresti su un menu vuoto poiché null significa "Usa l'impostazione predefinita")

Quindi ora l'utente può utilizzare il file normale ContextMenu proprietà quando IsEditing è falso...possono usare il IsEditingContextMenu quando IsEditing è vero e se non hanno specificato un IsEditingContextMenu, per la casella di testo viene utilizzato il valore predefinito interno che abbiamo definito.Poiché il menu contestuale della casella di testo non può mai essere effettivamente nullo, its ContextMenuOpening si attiva sempre e quindi la logica a supporto di questo comportamento funziona.

Come ho detto...Un vero dolore nel tentativo di capire tutto, ma dannazione se non ho una bella sensazione di realizzazione qui.

Spero che questo aiuti gli altri qui con lo stesso problema.Sentiti libero di rispondere qui o mandarmi un messaggio privato con domande.

Segno

Altri suggerimenti

Sfortunatamente stai cercando una soluzione semplice a un problema complesso. Il problema indicato è semplicemente avere controlli di interfaccia utente di impegno automatico che richiedono un minimo di interazione e "fare la cosa giusta" quando si "si allontana" da loro.

Il motivo per cui è complesso è perché qual è la cosa giusta dipende dal contesto dell'applicazione. L'approccio adotta WPF è di darti ai concetti logici di messa a fuoco e focus della tastiera e per farti decidere come fare la cosa giusta per te nella tua situazione.

Cosa succede se il menu contestuale viene aperto? Cosa dovrebbe accadere se il menu dell'applicazione è aperto? Cosa succede se il focus è passato a un'altra applicazione? Cosa succede se viene aperto un popup appartenente al controllo locale? Cosa succede se l'utente preme INVIO per chiudere una finestra di dialogo? Tutte queste situazioni possono essere gestite ma sono tutte via se si dispone di un pulsante di commit o l'utente deve premere Invio per commettere.

Quindi hai tre scelte:

  • Lascia che il controllo rimanga nello stato di modifica quando ha il focus logico
  • Aggiungi un commit esplicito o applicare meccanismo
  • Gestisci tutti i casi disordinati che si presentano quando si tenta di supportare l'auto-impegno

Non sarebbe solo più facile:

    void txtBox_LostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
    {
        TextBox txtBox = (sender as TextBox);

        if (e.NewFocus is ContextMenu && (e.NewFocus as ContextMenu).PlacementTarget == txtBox)
        {
            return;
        }

        // Rest of code for existing edit mode here...
    }

Non sono sicuro del problema del menu contestuale, ma stavo cercando di fare qualcosa di simile e ho scoperto che l'uso della cattura del mouse ti dà (quasi) il comportamento che stai cercando:

Vedi la risposta qui: Come può un controllo gestire un mouse fare clic fuori da quel controllo?

Non sono sicuro, ma questo potrebbe essere utile. Ho avuto un problema simile con la scatola combo modificabile. Il mio problema era che stavo usando il metodo di sovraccarico di Onlostfocus che non veniva chiamato. Fix era che avevo allegato un callback all'evento LostFocus e ha funzionato tutto bene.

Sono passato qui alla mia ricerca di una soluzione per un problema simile: ho un ListBox che perde concentrazione quando il ContextMenu Si apre e non voglio che ciò accada.

La mia semplice soluzione era impostare Focusable a False, entrambi per il ContextMenu e il suo MenuItemS:

<ContextMenu x:Key="QueryResultsMenu" Focusable="False">
    <ContextMenu.Resources>
        <Style TargetType="MenuItem">
            <Setter Property="Focusable" Value="False"/>
        </Style>
    </ContextMenu.Resources>
    <MenuItem ... />
</ContextMenu>

Spero che questo aiuti i futuri cercatori ...

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top