WPF Databind Antes De Guardar


En mi aplicación WPF, tengo una serie de cuadros de texto de databound. El UpdateSourceTrigger para estas encuadernaciones es LostFocus. El objeto se guarda mediante el menú Archivo. El problema que tengo es que es posible introducir un nuevo valor en un cuadro de texto, seleccionar Guardar en el menú Archivo, y nunca persistir el nuevo valor (el visible en el cuadro de texto) porque acceder al menú no elimina el foco del cuadro de texto. ¿Cómo puedo arreglar esto? ¿Hay alguna manera de forzar todos los controles en una página para databind?

@buen punto. Desafortunadamente, necesito usar LostFocus como mi UpdateSourceTrigger para soportar el tipo de validación que quiero.

@dmo: Había pensado en eso. Sin embargo, parece una solución realmente poco elegante para un problema relativamente simple. Además, requiere que haya un cierto control en la página que es siempre visible para recibir el foco. Sin embargo, mi aplicación está tabulada, por lo que no se presenta fácilmente dicho control sí mismo.

@Nidonocu: El hecho de que usar el menú no moviera el foco del cuadro de texto también me confundió. Ese es, sin embargo, el comportamiento que estoy viendo. El siguiente ejemplo simple demuestra mi 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; }
    }
}
Author: Spook, 2008-09-12

13 answers

Supongamos que tiene un cuadro de texto en una ventana y una barra de herramientas con un botón Guardar. Supongamos que la propiedad Text del cuadro de texto está vinculada a una propiedad en un objeto de negocio, y la propiedad UpdateSourceTrigger del enlace se establece en el valor predeterminado de LostFocus, lo que significa que el valor vinculado se devuelve a la propiedad de objeto de negocio cuando el cuadro de texto pierde el foco de entrada. Además, suponga que el botón Guardar de la barra de herramientas tiene su propiedad Comando establecida en ApplicationCommands.Guardar orden.

En esa situación, si edita el cuadro de texto y hace clic en el botón Guardar con el ratón, hay un problema. Al hacer clic en un botón en una barra de herramientas, el cuadro de texto no pierde el foco. Dado que el evento LostFocus del cuadro de texto no se activa, el enlace de propiedad de texto no actualiza la propiedad de origen del objeto de negocio.

Obviamente no debe validar y guardar un objeto si el valor editado más recientemente en la interfaz de usuario aún no se ha introducido en el objeto. Este es el exacto problema Karl había trabajado alrededor, escribiendo código en su ventana que buscaba manualmente un cuadro de texto con enfoque y actualizaba la fuente del enlace de datos. Su solución funcionó bien, pero me hizo pensar en una solución genérica que también sería útil fuera de este escenario en particular. Introduzca CommandGroup...

Tomado del artículo CodeProject de Josh Smith sobre CommandGroup

 6
Author: rudigrobler,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-12 07:22:48

Descubrí que la eliminación de los elementos de menú que son ámbito dependía del ámbito de enfoque del menú hace que el cuadro de texto pierda el enfoque correctamente. No creo que esto se aplique a TODOS los elementos del menú, pero ciertamente para una acción de guardar o validar.

<Menu FocusManager.IsFocusScope="False" >
 22
Author: BigBlondeViking,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-11-19 16:59:16

Suponiendo que hay más de un control en la secuencia de tabulación, la siguiente solución parece ser completa y general (solo cortar y pegar)...

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();
}
 14
Author: Dave the Rave,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-07-25 15:05:08

Este es un truco FEO, pero también debería funcionar

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

Este código comprueba si un cuadro de texto tiene foco... Si se encuentra 1... actualizar la fuente de enlace!

 7
Author: rudigrobler,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-12 07:32:13

La solución simple es actualizar el código Xaml como se muestra a continuación

    <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> 
 3
Author: Ram,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-09-22 17:24:23

¿Ha intentado configurar UpdateSourceTrigger a PropertyChanged? Alternativamente, puedes llamar al método UpdateSource (), pero eso parece un poco exagerado y frustra el propósito de TwoWay databinding.

 2
Author: palehorse,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-11 20:14:05

Me he encontrado con este problema y la mejor solución que he encontrado fue cambiar el valor enfocable del botón (o cualquier otro componente como MenuItem) a verdadero:

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

La razón por la que funciona, es porque obliga al botón a enfocarse antes de que invoque el comando y por lo tanto hace que el cuadro de texto o cualquier otro elemento UIElement para el caso pierda su enfoque y levante el evento de enfoque perdido que invoca el enlace para ser cambiado.

En caso de que esté utilizando bounded comando (como estaba señalando en mi ejemplo), la gran solución de John Smith no encajará muy bien ya que no se puede enlazar StaticExtension en la propiedad acotada (ni DP).

 2
Author: Tomer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-03-23 13:33:44

¿Podría establecer el enfoque en otro lugar justo antes de guardar?

Puede hacer esto llamando a focus() en un elemento de la interfaz de usuario.

Puedes enfocarte en cualquier elemento que invoque el "guardar". Si tu disparador es LostFocus entonces tienes que mover el foco a alguna parte. Guardar tiene la ventaja de que no se modifica y tendría sentido para el usuario.

 1
Author: dmo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-11 21:27:02

Al investigar esto para responderlo, estoy un poco confundido de que el comportamiento que está viendo está sucediendo, seguramente el acto de hacer clic en el menú Archivo o lo que tiene que desenfocar el cuadro de texto y configurarlo en el menú?

 0
Author: Nidonocu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-11 22:10:23

La forma más fácil es poner el foco en algún lugar.
Puedes volver a poner el foco inmediatamente, pero si pones el foco en cualquier lugar activará el evento LostFocus en cualquier tipo de control y hará que actualice su contenido:

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

Otra forma sería obtener el elemento enfocado, obtener el elemento de enlace del elemento enfocado y activar la actualización manualmente. Un ejemplo para TextBox y ComboBox (tendrías que añadir cualquier tipo de control que necesites soportar):

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();
 0
Author: Sam,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-10-23 14:06:40

¿Qué piensas de esto? Creo que he descubierto una manera de hacerlo un poco más genérico usando la reflexión. Realmente no me gustó la idea de mantener una lista como algunos de los otros ejemplos.

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

¿Ves algún problema con eso?

 0
Author: Shawn Nelson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-06-01 00:47:03

Dado que noté que este problema sigue siendo un dolor en el culo para resolver de una manera muy genérica, probé varias soluciones.

Finalmente uno que funcionó para mí: Siempre que sea necesario que los cambios en la interfaz de usuario se validen y actualicen a sus fuentes (Comprobar si hay cambios al cerrar una ventana, realizar operaciones de guardado, ...), llamo a una función de validación que hace varias cosas: - asegúrese de que un elemento enfocado (como cuadro de texto, combobox,...) pierde su enfoque que activará el valor predeterminado comportamiento de updatesource - validar cualquier control dentro del árbol del DependencyObject que se le da a la función de validación - volver a poner el foco en el elemento enfocado original

La función en sí devuelve true si todo está en orden (la validación es exitosa) -> su acción original (cerrando con opcional pidiendo confirmación, guardando, ... puede continuar. De lo contrario, la función devolverá false y su acción no podrá continuar porque hay errores de validación en uno o más elementos (con la ayuda de un ErrorTemplate genérico en los elementos).

El código (la funcionalidad de validación se basa en el artículo Detectando Errores de Validación de 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;
    }
}
 0
Author: Nathan Swannet,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:32:02

Estoy usando 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
}

Debería funcionar.

 0
Author: kenjiuno,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-11-13 06:27:24