質問
私の WPF アプリケーションには、データバインドされた TextBox が多数あります。の UpdateSourceTrigger
これらのバインディングの場合、 LostFocus
. 。オブジェクトは「ファイル」メニューを使用して保存されます。私が抱えている問題は、TextBox に新しい値を入力し、[File] メニューから [Save] を選択しても、メニューにアクセスしても TextBox からフォーカスが削除されないため、新しい値 (TextBox に表示される値) を永続化できないことです。 。これを修正するにはどうすればよいですか?ページ内のすべてのコントロールを強制的にデータバインドする方法はありますか?
@palehorse:いい視点ね。残念ながら、必要な種類の検証をサポートするには、UpdateSourceTrigger として LostFocus を使用する必要があります。
@dmo:私はそれを考えていました。しかし、これは比較的単純な問題に対する非常に洗練されていない解決策のように思えます。また、フォーカスを受け取るために常に表示されるページ上に何らかのコントロールが必要です。ただし、私のアプリケーションはタブ化されているため、そのようなコントロールはすぐには表示されません。
@ニドノク:メニューを使用しても 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; }
}
}
解決
ウィンドウに TextBox があり、その中に Save ボタンのある ToolBar があるとします。TextBox の Text プロパティがビジネス オブジェクトのプロパティにバインドされており、バインディングの UpdateSourceTrigger プロパティがデフォルト値の LostFocus に設定されているとします。これは、TextBox が入力フォーカスを失ったときにバインドされた値がビジネス オブジェクトのプロパティにプッシュバックされることを意味します。また、ツールバーの [保存] ボタンの Command プロパティが ApplicationCommands.Save コマンドに設定されていると仮定します。
その状況で、TextBox を編集し、マウスで [保存] ボタンをクリックすると、問題が発生します。ツールバーのボタンをクリックしても、TextBox はフォーカスを失いません。TextBox の LostFocus イベントが発生しないため、Text プロパティ バインディングはビジネス オブジェクトのソース プロパティを更新しません。
UI で最後に編集された値がまだオブジェクトにプッシュされていない場合は、オブジェクトを検証して保存しないでください。これはまさにカールが、フォーカスのある TextBox を手動で検索し、データ バインディングのソースを更新するコードをウィンドウに記述することで回避した問題です。彼のソリューションはうまく機能しましたが、この特定のシナリオ以外でも役立つ一般的なソリューションについて考えるようになりました。コマンドグループを入力してください…
Josh Smith の CodeProject の記事から引用 コマンドグループ
他のヒント
スコープに依存するメニュー項目をメニューの FocusScope から削除すると、テキストボックスが正しくフォーカスを失うことがわかりました。これはメニュー内のすべての項目に当てはまるとは思いませんが、保存または検証アクションには確かに当てはまります。
<Menu FocusManager.IsFocusScope="False" >
タブ シーケンスに複数のコントロールがあると仮定すると、次の解決策は完全かつ一般的であるように見えます (カット アンド ペーストするだけです)。
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();
}
これは醜いハックですが、うまくいくはずです
TextBox focusedTextBox = Keyboard.FocusedElement as TextBox;
if (focusedTextBox != null)
{
focusedTextBox.GetBindingExpression(TextBox.TextProperty).UpdateSource();
}
このコードは、TextBox にフォーカスがあるかどうかをチェックします。1が見つかったら…バインディングソースを更新してください!
簡単な解決策は、以下に示すように 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>
UpdateSourceTrigger を PropertyChanged に設定してみましたか?あるいは、UpdateSOurce() メソッドを呼び出すこともできますが、それは少しやりすぎであり、TwoWay データバインディングの目的に反します。
私はこの問題に遭遇しました。私が見つけた最良の解決策は、ボタン (または MenuItem などの他のコンポーネント) のフォーカス可能な値を true に変更することでした。
<Button Focusable="True" Command="{Binding CustomSaveCommand}"/>
これが機能する理由は、コマンドを呼び出す前にボタンが強制的にフォーカスされるため、TextBox が作成されるためです。 またはその他の UIElement フォーカスを失い、変更されるバインディングを呼び出す Lost focus イベントを発生させます。
制限付きコマンドを使用している場合 (例で示したように)、制限付きプロパティ (または DP) に StaticExtension をバインドできないため、John Smith の優れたソリューションはあまり適合しません。
保存する直前に別の場所にフォーカスを設定していただけますか?
これを行うには、UI 要素で focus() を呼び出します。
「保存」を呼び出す要素に焦点を当てることができます。トリガーが LostFocus の場合は、フォーカスをどこかに移動する必要があります。保存には、変更されず、ユーザーにとって意味があるという利点があります。
これに答えるために調査していると、あなたが見ている動作が起こっているのか、確かに[ファイル]メニューをクリックする動作なのか、それともテキストボックスのフォーカスを解除してメニューに設定する必要があるのか、少し混乱しています。
最も簡単な方法は、 どこかに焦点を設定する.
フォーカスをすぐに戻すこともできますが、どこかにフォーカスを設定すると、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();
あなたはこのことについてどう思いますか?リフレクションを使用してもう少し一般的なものにする方法を見つけたと思います。私は、他の例のようにリストを維持するという考えが本当に好きではありませんでした。
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);
}
}
}
それに何か問題はありますか?
この問題を非常に一般的な方法で解決するのは依然として面倒であることに気づいたので、さまざまな解決策を試しました。
最終的に私にとってうまくいったのは次のとおりです。UI の変更を検証してソースに更新する必要がある場合 (ウィンドウを閉じるとき、保存操作を実行するときに変更を確認するなど)、さまざまな処理を実行する検証関数を呼び出します。- 焦点を絞った要素(Textbox、Combobox、...など)がフォーカスを失うことを確認してください。デフォルトのUpdatesOurce動作をトリガーする - 検証関数に与えられたDependencyObjectのツリー内のコントロールを検証します - 元のフォーカスにフォーカスを設定します要素
すべてが正常である (検証が成功した) 場合、関数自体は true を返します。 -> 元のアクション (オプションで確認を求めて終了、保存など) を続行できます。それ以外の場合、関数は false を返し、1 つ以上の要素に検証エラーがあるため、アクションは続行できません (要素の汎用 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;
}
}
バインディンググループを使用しています。
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
}
うまくいくはずです。