1

再現プロジェクトをダウンロード

編集

解決策が機能しているため、最初はなぜこれが機能しているのかという最初の疑問から完全に脱却することがありました。結局、項目はビジュアル ツリーの一部ではありません。結局、それは完全に理にかなっています:

  • そのコレクションのボタンはビジュアル ツリーにないため、要素バインディングは機能しません。
  • テンプレートを適用すると、テンプレートがビジュアル ツリーに配置され、バインドがこの時点で適用されている場合は、作業が開始されます。
  • これにより、疑わしい競合状態が確認されます。

私の同僚は、問題を示す拡張デバッグをいくつか行いました。バインディングが成功した場合、OnApplyBinding が最初に呼び出されました。したがって、論理ツリーを調整せずにコレクションを使用することは、単に欠陥がありました。

正しい道に戻った返信をありがとう!


元の投稿

ObservableCollection を公開するビュー コントロールがあります。ビューには、ボタンなどの任意の要素を含めることができます。ボタンのElementNameバインディングに注意してください。

<local:ViperView>    
    <local:ViperView.MenuItems>
        <Button Content="{Binding ElementName=btn, Path=Content}" />
    </local:ViperView.MenuItems>

    <Grid>
        <Button x:Name="btn" Content="HELLO WORLD" />
    </Grid>
</local:ViperView>

コントロールの ControlTemplate は、ItemsControl を使用してコンテンツをレンダリングするだけです。

<ControlTemplate ...
...

    <ItemsControl
        x:Name="PART_NavigationMenuItemsHost"
        ItemsSource="{Binding MenuItems, RelativeSource={RelativeSource TemplatedParent}}" />
 </ControlTemplate>

上記のビューは、メイン ビュー モデルの ActiveView プロパティに割り当てられています。メイン ウィンドウは、データ バインディングを介してビューを表示するだけです。

ここでの問題:ビューが作成後にすぐにビュー モデルに割り当てられない場合、そのビュー内の ElementName バインディングは確実に機能しません。

ここに画像の説明を入力

ElementName バインディングは次のように機能します。

MainViewModel.ActiveView = new ViperView();

ElementName バインディングは、通常の優先度を使用して動作することがあります。

var view = new ViperView();
Dispatcher.BeginInvoke(() => MainViewModel.ActiveView  view);

ビュー モデル プロパティが低い優先度に設定されている場合、ElementName バインディングは常に失敗します。

var view = new ViperView();
Dispatcher.BeginInvoke(DispatcherPriority.Render, () => MainViewModel.ActiveView = view);

ElementName バインディングは、プロパティがワーカー スレッドから設定されている場合に機能することがあります (バインディング エンジンは UI スレッドにマーシャリングします)。

var view = new ViperView();
Task.Factory.StartNew(() => MainViewModel.ActiveView = view);

ワーカー スレッドに遅延がある場合、ElementName バインディングは常に失敗します。

var view = new ViperView();
var view = new ViperView();
Task.Factory.StartNew(() => 
{
    Thread.Sleep(100);
    MainViewModel.ActiveView = view;
});

これに対する答えはありません。タイミングが関係しているようです。たとえば、上記の Task サンプルに短い Thread.Sleep を追加すると、常にバインドが壊れますが、スリープがないと、時々壊れるだけです。

これは私にとってかなりのショーストッパーです-どんなポインタでも大歓迎です...

アドバイスありがとうフィリップ

4

3 に答える 3

0

サンプルのボタンが親ビューのコレクションに追加される前に ElementName バインディングが部分的に失敗することを考えると、バインディングをインターセプトするためにできることはあまりありません。少し汚い回避策は、テンプレートが適用され、ビジュアル ツリーが確立されたら、これらのコントロールのバインドを更新することです。

OnApplyTemplate の前で、以下を呼び出します。

internal static class BindingUtil
{
    /// <summary>
    /// Recursively resets all ElementName bindings on the submitted object
    /// and its children.
    /// </summary>
    public static void ResetElementNameBindings(this DependencyObject obj)
    {
        IEnumerable boundProperties = obj.GetDataBoundProperties();
        foreach (DependencyProperty dp in boundProperties)
        {
            Binding binding = BindingOperations.GetBinding(obj, dp);
            if (binding != null && !String.IsNullOrEmpty(binding.ElementName)) //binding itself should never be null, but anyway
            {
                //just updating source and/or target doesn’t do the trick – reset the binding
                BindingOperations.ClearBinding(obj, dp);
                BindingOperations.SetBinding(obj, dp, binding);
            }
        }

        int count = VisualTreeHelper.GetChildrenCount(obj);
        for (int i = 0; i < count; i++)
        {
            //process child items recursively
            DependencyObject childObject = VisualTreeHelper.GetChild(obj, i);
            ResetElementNameBindings(childObject);
        }
    }


    public static IEnumerable GetDataBoundProperties(this DependencyObject element)
    {
        LocalValueEnumerator lve = element.GetLocalValueEnumerator();

        while (lve.MoveNext())
        {
            LocalValueEntry entry = lve.Current;
            if (BindingOperations.IsDataBound(element, entry.Property))
            {
                yield return entry.Property;
            }
        }
    }
}

もう 1 つの修正方法として、実行時に論理ツリーを変更することをお勧めします。以下のコードをビューに追加すると、問題も解決します。

public class ViperView : ContentControl
{
    private readonly ObservableCollection<object> menuItems = new ObservableCollection<object>();

    public ObservableCollection<object> NavigationMenuItems
    {
        get { return menuItems; }
    }


    public ViperView()
    {
        NavigationMenuItems.CollectionChanged += OnMenuItemsChanged;
    }

    private void OnMenuItemsChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.NewItems != null)
        {
            foreach (var newItem in e.NewItems)
            {
                AddLogicalChild(newItem);
            }
        }
    }

    protected override IEnumerator LogicalChildren
    {
        get
        {
            yield return this.Content;
            foreach (var mi in NavigationMenuItems)
            {
                yield return mi;
            }
        }
    }
}
于 2012-09-22T14:35:10.127 に答える
0

私の知る限り、ElementNameバインディングはいつでも更新されません。一度だけプロパティにバインドされ、その後更新が停止します。これはここであなたの問題を説明するかもしれません: タイムスタンプに応じて最初のバインディングが発生します (または発生しません)。

UpdateSourceTriggerバインディングのプロパティを指定することで修正できる変更があります。

<Button Content="{Binding ElementName=btn, Path=Content, UpdateSourceTrigger=PropertyChanged}" />

これにより、btn.Content が更新されるたびにバインドが更新されます。

これがうまくいくことを願っています=)

于 2012-09-21T17:56:57.463 に答える
0

最初のオプションが機能する理由を完全に説明することはできません。ただし、他のものが機能しない理由を説明できます。

まず、ElementName は、要素が同じビジュアル ツリーにある場合にのみ機能します。NavigationButtonItems は、ViperView の実際のコンテンツから分離されていることに注意してください。

したがって、次のようにします。

<Button Content="{Binding ActualWidth, 
            RelativeSource={RelativeSource AncestorType=WpfApplication2:ViperView}}" />

(NavigationButtonItems の一部です)。NavigationButtonItems と ViperView アイテムが 1 つにブレンドされていない場合 (1 つのビジュアル ツリーへのブレンドは ControlTemplate で発生します)、このバインディングは失敗し、失敗したままになります。

ここで、バインディングが行われているときにビジュアル ツリーがたまたま 1 であるとします。その後、バインディングが成功し、すべてがうまくいきます。

コンテンツをレンダリングすると、1 つのビジュアル ツリーにブレンドされることに注意してください。

これを少し改善する方法を示す簡単な例を次に示します。

  <Button

            Background="Purple"
            Width="100"
            Height="20"
             >
            <Button.Style>
                <Style TargetType="Button">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding IsReady}" Value="True">
                            <Setter Property="Content" Value="{Binding ActualWidth, 
            RelativeSource={RelativeSource AncestorType=WpfApplication2:ViperView}}" />
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </Button.Style>
        </Button>

IsReady プロパティは viewModel にある必要があり、基本的に「はい、すべてが 1 つのビジュアル ツリーとしてレンダリングされ、バインディングを適用できます」と通知します。

IsReady トリックを実行すると、ActualWidth が機能し始めます。

    Task.Factory.StartNew(() =>
                              {
                                  Thread.Sleep(10);
                                  dc.ActiveScreen = view;
                                  //ps you might need to force wpf finish with  rendering here. like use Application.DoEvents()
                                  dc.IsReady = true;//force bindings since everything is one now.
                              });

説明が必要な場合はお知らせください。

于 2012-09-21T18:49:36.670 に答える