Связка данных WPF перед сохранением

В моем приложении WPF у меня есть несколько текстовых полей с привязкой к данным. UpdateSourceTrigger для этих привязок - LostFocus. Объект сохраняется с помощью меню «Файл». У меня проблема в том, что можно ввести новое значение в TextBox, выбрать Сохранить в меню File и никогда не сохранять новое значение (то, которое отображается в TextBox), потому что доступ к меню не удаляет фокус из TextBox . Как я могу это исправить? Есть ли способ принудительно привязать все элементы управления на странице к данным?

@palehorse: Хорошее замечание. К сожалению, мне нужно использовать LostFocus в качестве UpdateSourceTrigger, чтобы поддерживать нужный мне тип проверки.

@dmo: Я думал об этом. Однако это кажется действительно неэлегантным решением относительно простой проблемы. Кроме того, требуется, чтобы на странице был какой-то элемент управления, который всегда виден для получения фокуса. Однако в моем приложении есть вкладки, поэтому такой элемент управления сразу не появляется.

@Nidonocu: Меня тоже смутил тот факт, что использование меню не перемещает фокус из TextBox. Однако я наблюдаю именно такое поведение. Следующий простой пример демонстрирует мою проблему:

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

person Joseph Sturtevant    schedule 11.09.2008    source источник


Ответы (12)


Предположим, у вас есть TextBox в окне и ToolBar с кнопкой «Сохранить». Предположим, что свойство Text TextBox привязано к свойству бизнес-объекта, а для свойства UpdateSourceTrigger привязки задано значение по умолчанию LostFocus, что означает, что связанное значение передается обратно свойству бизнес-объекта, когда TextBox теряет фокус ввода. Также предположим, что для кнопки «Сохранить» на панели инструментов установлено свойство Command, равное ApplicationCommands.Save command.

В этой ситуации, если вы отредактируете TextBox и нажмете кнопку «Сохранить» с помощью мыши, возникнет проблема. При нажатии кнопки на панели инструментов текстовое поле не теряет фокус. Поскольку событие LostFocus TextBox не запускается, привязка свойства Text не обновляет исходное свойство бизнес-объекта.

Очевидно, вам не следует проверять и сохранять объект, если последнее измененное значение в пользовательском интерфейсе еще не было помещено в объект. Это именно та проблема, которую Карл решил решить, написав в своем окне код, который вручную искал TextBox с фокусом и обновлял источник привязки данных. Его решение работало нормально, но заставило меня задуматься об общем решении, которое также было бы полезно за пределами этого конкретного сценария. Введите CommandGroup…

Взято из статьи Джоша Смита CodeProject о CommandGroup

person rudigrobler    schedule 12.09.2008
comment
Это решение работает только для TextBox. Есть ли способ заставить его работать для любого контроля? - person Kevin Berridge; 24.09.2008

Я обнаружил, что удаление элементов меню, область действия которых зависит от FocusScope меню, приводит к правильной потере фокуса текстовым полем. Я бы не подумал, что это применимо ко ВСЕМ пунктам меню, но определенно для действия сохранения или проверки.

<Menu FocusManager.IsFocusScope="False" >
person BigBlondeViking    schedule 19.11.2009
comment
Это решает исходную проблему (ту же, что и у меня). Мой сеттер теперь вызывается при нажатии кнопок меню, и я могу оставить LostFocus как UpdateSourceTrigger. - person Bob; 17.03.2010
comment
На мой взгляд, это лучшее из представленных решений. Спасибо. - person Ignacio Soler Garcia; 22.06.2011
comment
о, вау ... я ответил на это давным-давно ~ рад, что смог помочь :) - person BigBlondeViking; 24.06.2011
comment
+1 Я сейчас задаю похожие вопросы, чтобы убедиться, что этот ответ присутствует, потому что он лучший! - person surfen; 01.04.2012
comment
Это оставит фокус на кнопке панели инструментов. - person synergetic; 19.04.2013
comment
Кажется, что один из принципов дизайна нарушается, если делать это таким образом. Само поведение одного элемента управления зависит от того, готовы ли другие элементы управления принять фокус. - person Cary; 09.05.2016

Предполагая, что в последовательности вкладок есть более одного элемента управления, следующее решение кажется полным и общим (просто вырезать и вставить) ...

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();
}
person Dave the Rave    schedule 18.01.2011
comment
В каком методе следует добавить этот фрагмент кода? Этот способ кажется принудительным, независимо от того, хочет ли другой элемент управления принять фокус или нет, этот код заставляет текущий выбранный элемент управления взять фокус. - person Cary; 09.05.2016
comment
Я использовал этот фрагмент кода в методе выполнения кнопки «Сохранить». Затем другие текстовые поля теряются, и источник обновления запускается, связанный с этим свойством. - person dush88c; 03.10.2016

Это УЖАСНЫЙ взлом, но он также должен работать

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

Этот код проверяет, имеет ли TextBox фокус ... Если найден 1 ... обновите источник привязки!

person rudigrobler    schedule 12.09.2008
comment
+1, это обходной путь, который я использую сейчас. Чтобы сделать его менее уродливым, я рекомендую проверить выражение привязки для null перед вызовом UpdateSource (поскольку в настоящее время в фокусе может находиться несвязанный TextBox). - person Heinzi; 09.02.2011

Простое решение - обновить код Xaml, как показано ниже.

    <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> 
person Ram    schedule 22.09.2011

Вы пробовали установить для UpdateSourceTrigger значение PropertyChanged? В качестве альтернативы вы можете вызвать метод UpdateSOurce (), но это кажется излишним и сводит на нет цель привязки данных TwoWay.

person palehorse    schedule 11.09.2008

Я столкнулся с этой проблемой, и лучшим решением, которое я нашел, было изменить фокусируемое значение кнопки (или любого другого компонента, такого как MenuItem) на true:

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

Причина, по которой он работает, заключается в том, что он заставляет кнопку фокусироваться перед вызовом команды и, следовательно, заставляет TextBox или любой другой UIElement в этом отношении терять фокус и вызывать событие потери фокуса, которое вызывает привязка должна быть изменена.

В случае, если вы используете ограниченную команду (как я указывал в своем примере), отличное решение Джона Смита не очень хорошо подойдет, поскольку вы не можете привязать StaticExtension к ограниченному свойству (или DP).

person Tomer    schedule 23.03.2012

Не могли бы вы установить фокус на другом месте непосредственно перед сохранением?

Вы можете сделать это, вызвав focus () для элемента пользовательского интерфейса.

Вы можете сосредоточиться на любом элементе, вызывающем «сохранение». Если ваш триггер - LostFocus, вам нужно куда-нибудь переместить фокус. Преимущество сохранения заключается в том, что оно не изменяется и имеет смысл для пользователя.

person dmo    schedule 11.09.2008

Поскольку я заметил, что эта проблема все еще является головной болью, которую нужно решить очень общим способом, я пробовал различные решения.

В конце концов, это сработало для меня: всякий раз, когда возникает необходимость, чтобы изменения пользовательского интерфейса должны быть проверены и обновлены до его источников (проверка изменений при закрытии окна, выполнении операций сохранения, ...), я вызываю функцию проверки, которая выполняет различные вещи: - убедитесь, что элемент с фокусом (например, текстовое поле, поле со списком, ...) теряет фокус, что вызовет поведение источника обновлений по умолчанию - проверьте любые элементы управления в дереве объекта DependencyObject, который передается функции проверки - установите фокус обратно на исходный сфокусированный элемент

Сама функция возвращает истину, если все в порядке (проверка прошла успешно) -> ваше исходное действие (закрытие с дополнительным запросом подтверждения, сохранение, ...) может продолжаться. В противном случае функция вернет false, и ваше действие не может быть продолжено из-за ошибок проверки для одного или нескольких элементов (с помощью универсального шаблона ErrorTemplate для элементов).

Код (функциональность проверки основана на статье Обнаружение ошибок проверки 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;
    }
}
person Nathan Swannet    schedule 30.12.2011

Самый простой способ - установить фокус где-нибудь.
Вы можете вернуть фокус немедленно, но установка фокуса в любом месте вызовет событие LostFocus для любого типа управления и заставьте его обновить свои данные:

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

Другой способ - получить элемент с фокусом, получить элемент привязки из элемента с фокусом и запустить обновление вручную. Пример для TextBox и ComboBox (вам нужно будет добавить любой тип элемента управления, который вам нужен):

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();
person Sam    schedule 23.10.2008

Что Вы думаете об этом? Я считаю, что нашел способ сделать его более универсальным, используя отражение. Мне действительно не понравилась идея вести список, как в некоторых других примерах.

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

Видите какие-нибудь проблемы с этим?

person Shawn Nelson    schedule 01.06.2011

Я использую 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
}

Он должен работать.

person kenjiuno    schedule 13.11.2014