Pergunta

No meu aplicativo WPF, tenho vários TextBoxes vinculados a dados.O UpdateSourceTrigger para essas ligações é LostFocus.O objeto é salvo usando o menu Arquivo.O problema que tenho é que é possível inserir um novo valor em um TextBox, selecionar Salvar no menu Arquivo e nunca persistir o novo valor (aquele visível no TextBox) porque acessar o menu não remove o foco do TextBox .Como posso consertar isso?Existe alguma maneira de forçar a ligação de dados de todos os controles de uma página?

@cavalo pálido:Bom ponto.Infelizmente, preciso usar LostFocus como meu UpdateSourceTrigger para oferecer suporte ao tipo de validação que desejo.

@dmo:Eu tinha pensado nisso.Parece, no entanto, uma solução realmente deselegante para um problema relativamente simples.Além disso, requer que haja algum controle na página que esteja sempre visível para receber o foco.Meu aplicativo é tabulado, portanto, esse controle não se apresenta prontamente.

@Nidonocu:O fato de usar o menu não ter tirado o foco do TextBox também me confundiu.Esse é, no entanto, o comportamento que estou vendo.O exemplo simples a seguir demonstra meu problema:

<Window x:Class="WpfApplication2.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300">
    <Window.Resources>
        <ObjectDataProvider x:Key="MyItemProvider" />
    </Window.Resources>
    <DockPanel LastChildFill="True">
        <Menu DockPanel.Dock="Top">
            <MenuItem Header="File">
                <MenuItem Header="Save" Click="MenuItem_Click" />
            </MenuItem>
        </Menu>
        <StackPanel DataContext="{Binding Source={StaticResource MyItemProvider}}">
            <Label Content="Enter some text and then File > Save:" />
            <TextBox Text="{Binding ValueA}" />
            <TextBox Text="{Binding ValueB}" />
        </StackPanel>
    </DockPanel>
</Window>
using System;
using System.Text;
using System.Windows;
using System.Windows.Data;

namespace WpfApplication2
{
    public partial class Window1 : Window
    {
        public MyItem Item
        {
            get { return (FindResource("MyItemProvider") as ObjectDataProvider).ObjectInstance as MyItem; }
            set { (FindResource("MyItemProvider") as ObjectDataProvider).ObjectInstance = value; }
        }

        public Window1()
        {
            InitializeComponent();
            Item = new MyItem();
        }

        private void MenuItem_Click(object sender, RoutedEventArgs e)
        {
            MessageBox.Show(string.Format("At the time of saving, the values in the TextBoxes are:\n'{0}'\nand\n'{1}'", Item.ValueA, Item.ValueB));
        }
    }

    public class MyItem
    {
        public string ValueA { get; set; }
        public string ValueB { get; set; }
    }
}
Foi útil?

Solução

Suponha que você tenha um TextBox em uma janela e uma ToolBar com um botão Salvar.Suponha que a propriedade Text do TextBox esteja vinculada a uma propriedade em um objeto de negócios e que a propriedade UpdateSourceTrigger da ligação esteja definida como o valor padrão de LostFocus, o que significa que o valor vinculado será enviado de volta à propriedade do objeto de negócios quando o TextBox perder o foco de entrada.Além disso, suponha que o botão Salvar da barra de ferramentas tenha sua propriedade Command definida como o comando ApplicationCommands.Save.

Nessa situação, se você editar o TextBox e clicar no botão Salvar com o mouse, há um problema.Ao clicar em um Button em uma ToolBar, o TextBox não perde o foco.Como o evento LostFocus do TextBox não é acionado, a ligação da propriedade Text não atualiza a propriedade source do objeto de negócio.

Obviamente você não deve validar e salvar um objeto se o valor editado mais recentemente na UI ainda não tiver sido inserido no objeto.Este é exatamente o problema que Karl resolveu, escrevendo código em sua janela que procurava manualmente por um TextBox com foco e atualizava a fonte da ligação de dados.A solução dele funcionou bem, mas me fez pensar em uma solução genérica que também seria útil fora desse cenário específico.Entre no Grupo de Comandos…

Retirado do artigo CodeProject de Josh Smith sobre Grupo de comando

Outras dicas

Descobri que remover os itens de menu cujo escopo depende do FocusScope do menu faz com que a caixa de texto perca o foco corretamente.Eu não acho que isso se aplica a TODOS os itens do Menu, mas certamente para uma ação de salvar ou validar.

<Menu FocusManager.IsFocusScope="False" >

Supondo que haja mais de um controle na sequência de guias, a solução a seguir parece completa e geral (basta recortar e colar)...

Control currentControl = System.Windows.Input.Keyboard.FocusedElement as Control;

if (currentControl != null)
{
    // Force focus away from the current control to update its binding source.
    currentControl.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
    currentControl.Focus();
}

Este é um hack FEIO, mas também deve funcionar

TextBox focusedTextBox = Keyboard.FocusedElement as TextBox;
if (focusedTextBox != null)
{
    focusedTextBox.GetBindingExpression(TextBox.TextProperty).UpdateSource();
}

Este código verifica se um TextBox tem foco...Se 1 for encontrado...atualize a fonte de ligação!

A solução simples é atualizar o código Xaml conforme mostrado abaixo

    <StackPanel DataContext="{Binding Source={StaticResource MyItemProvider}}"> 
        <Label Content="Enter some text and then File > Save:" /> 
        <TextBox Text="{Binding ValueA, UpdateSourceTrigger=PropertyChanged}" /> 
        <TextBox Text="{Binding ValueB, UpdateSourceTrigger=PropertyChanged}" /> 
    </StackPanel> 

Você já tentou definir UpdateSourceTrigger como PropertyChanged?Como alternativa, você poderia chamar o método UpdateSource(), mas isso parece um pouco exagerado e anula o propósito da ligação de dados TwoWay.

Encontrei esse problema e a melhor solução que encontrei foi alterar o valor focalizável do botão (ou qualquer outro componente, como MenuItem) para verdadeiro:

<Button Focusable="True" Command="{Binding CustomSaveCommand}"/>

A razão pela qual funciona é porque força o botão a ficar focado antes de invocar o comando e, portanto, torna o TextBox ou qualquer outro UIElement para esse assunto perder o foco e gerar um evento de foco perdido que invoca a alteração da ligação.

Caso você esteja usando um comando limitado (como eu estava apontando no meu exemplo), a ótima solução de John Smith não se encaixará muito bem, pois você não pode vincular StaticExtension a uma propriedade limitada (nem DP).

Você poderia definir o foco em outro lugar antes de salvar?

Você pode fazer isso chamando focus() em um elemento da UI.

Você pode se concentrar em qualquer elemento que invoque o "salvar".Se o seu gatilho for LostFocus, você precisará mover o foco para algum lugar.Salvar tem a vantagem de não ser modificado e fazer sentido para o usuário.

Ao pesquisar isso para responder, estou um pouco confuso sobre o comportamento que você está vendo estar acontecendo, certamente o ato de clicar no menu Arquivo ou o que você deveria desfocar a caixa de texto e configurá-la para o menu?

A maneira mais fácil é coloque o foco em algum lugar.
Você pode definir o foco de volta imediatamente, mas definir o foco em qualquer lugar irá acionar o evento LostFocus em qualquer tipo de controle e faça com que ele atualize suas coisas:

IInputElement x = System.Windows.Input.Keyboard.FocusedElement;
DummyField.Focus();
x.Focus();

Outra maneira seria obter o elemento em foco, obter o elemento de ligação do elemento em foco e acionar a atualização manualmente.Um exemplo para TextBox e ComboBox (você precisaria adicionar qualquer tipo de controle necessário para suporte):

TextBox t = Keyboard.FocusedElement as TextBox;
if ((t != null) && (t.GetBindingExpression(TextBox.TextProperty) != null))
  t.GetBindingExpression(TextBox.TextProperty).UpdateSource();

ComboBox c = Keyboard.FocusedElement as ComboBox;
if ((c != null) && (c.GetBindingExpression(ComboBox.TextProperty) != null))
  c.GetBindingExpression(ComboBox.TextProperty).UpdateSource();

O que você pensa sobre isso?Acredito que descobri uma maneira de torná-lo um pouco mais genérico usando reflexão.Eu realmente não gostei da ideia de manter uma lista como alguns dos outros exemplos.

var currentControl = System.Windows.Input.Keyboard.FocusedElement;
if (currentControl != null)
{
    Type type = currentControl.GetType();
    if (type.GetMethod("MoveFocus") != null && type.GetMethod("Focus") != null)
    {
        try
        {
            type.GetMethod("MoveFocus").Invoke(currentControl, new object[] { new TraversalRequest(FocusNavigationDirection.Next) });
            type.GetMethod("Focus").Invoke(currentControl, null);
        }
        catch (Exception ex)
        {
            throw new Exception("Unable to handle unknown type: " + type.Name, ex);
        }
    }
}

Vê algum problema com isso?

Como percebi que esse problema ainda é difícil de resolver de uma forma muito genérica, tentei várias soluções.

Eventualmente, um que funcionou para mim:Sempre que houver necessidade de que as alterações da IU sejam validadas e atualizadas em suas fontes (verificar alterações ao fechar uma janela, executar operações Salvar, ...), eu chamo uma função de validação que faz várias coisas:- Verifique se um elemento focado (como TextBox, ComboBox, ...) perde seu foco que desencadeará o comportamento padrão das atualizações - validar quaisquer controles dentro da árvore do dependencyObject, que é dada à função de validação - defina o foco de volta para o original focado elemento

A própria função retorna verdadeiro se tudo estiver em ordem (a validação foi bem-sucedida) -> sua ação original (fechar com pedido opcional de confirmação, salvar, ...) pode continuar.Caso contrário, a função retornará falso e sua ação não poderá continuar porque há erros de validação em um ou mais elementos (com a ajuda de um ErrorTemplate genérico nos elementos).

O código (a funcionalidade de validação é baseada no artigo Detectando erros de validação WPF):

public static class Validator
{
    private static Dictionary<String, List<DependencyProperty>> gdicCachedDependencyProperties = new Dictionary<String, List<DependencyProperty>>();

    public static Boolean IsValid(DependencyObject Parent)
    {
        // Move focus and reset it to update bindings which or otherwise not processed until losefocus
        IInputElement lfocusedElement = Keyboard.FocusedElement;
        if (lfocusedElement != null && lfocusedElement is UIElement)
        {
            // Move to previous AND to next InputElement (if your next InputElement is a menu, focus will not be lost -> therefor move in both directions)
            (lfocusedElement as UIElement).MoveFocus(new TraversalRequest(FocusNavigationDirection.Previous));
            (lfocusedElement as UIElement).MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
            Keyboard.ClearFocus();
        }

        if (Parent as UIElement == null || (Parent as UIElement).Visibility != Visibility.Visible)
            return true;

        // Validate all the bindings on the parent 
        Boolean lblnIsValid = true;
        foreach (DependencyProperty aDependencyProperty in GetAllDependencyProperties(Parent))
        {
            if (BindingOperations.IsDataBound(Parent, aDependencyProperty))
            {
                // Get the binding expression base. This way all kinds of bindings (MultiBinding, PropertyBinding, ...) can be updated
                BindingExpressionBase lbindingExpressionBase = BindingOperations.GetBindingExpressionBase(Parent, aDependencyProperty);
                if (lbindingExpressionBase != null)
                {
                    lbindingExpressionBase.ValidateWithoutUpdate();
                    if (lbindingExpressionBase.HasError)
                        lblnIsValid = false;
                }
            }
        }

        if (Parent is Visual || Parent is Visual3D)
        {
            // Fetch the visual children (in case of templated content, the LogicalTreeHelper will return no childs)
            Int32 lintVisualChildCount = VisualTreeHelper.GetChildrenCount(Parent);
            for (Int32 lintVisualChildIndex = 0; lintVisualChildIndex < lintVisualChildCount; lintVisualChildIndex++)
                if (!IsValid(VisualTreeHelper.GetChild(Parent, lintVisualChildIndex)))
                    lblnIsValid = false;
        }

        if (lfocusedElement != null)
            lfocusedElement.Focus();

        return lblnIsValid;
    }

    public static List<DependencyProperty> GetAllDependencyProperties(DependencyObject DependencyObject)
    {
        Type ltype = DependencyObject.GetType();
        if (gdicCachedDependencyProperties.ContainsKey(ltype.FullName))
            return gdicCachedDependencyProperties[ltype.FullName];

        List<DependencyProperty> llstDependencyProperties = new List<DependencyProperty>();
        List<FieldInfo> llstFieldInfos = ltype.GetFields(BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Static).Where(Field => Field.FieldType == typeof(DependencyProperty)).ToList();
        foreach (FieldInfo aFieldInfo in llstFieldInfos)
            llstDependencyProperties.Add(aFieldInfo.GetValue(null) as DependencyProperty);
        gdicCachedDependencyProperties.Add(ltype.FullName, llstDependencyProperties);

        return llstDependencyProperties;
    }
}

Estou usando o BindingGroup.

XAML:

<R:RibbonWindow Closing="RibbonWindow_Closing" ...>

    <FrameworkElement.BindingGroup>
        <BindingGroup />
    </FrameworkElement.BindingGroup>

    ...
</R:RibbonWindow>

C#

private void RibbonWindow_Closing(object sender, CancelEventArgs e) {
    e.Cancel = !NeedSave();
}

bool NeedSave() {
    BindingGroup.CommitEdit();

    // Insert your business code to check modifications.

    // return true; if Saved/DontSave/NotChanged
    // return false; if Cancel
}

Deveria funcionar.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top