読み取り専用GUIプロパティをViewModelに戻す


124

Viewからのいくつかの読み取り専用の依存関係プロパティの現在の状態を常に把握しているViewModelを書きたいのですが。

具体的には、私のGUIにはFlowDocumentPageViewerが含まれており、これはFlowDocumentから一度に1ページを表示します。FlowDocumentPageViewerは、CanGoToPreviousPageおよびCanGoToNextPageと呼ばれる2つの読み取り専用の依存関係プロパティを公開します。私は自分のViewModelが常にこれら2つのViewプロパティの値を知っていることを望みます。

私はOneWayToSourceデータバインディングでこれを行うことができると考えました:

<FlowDocumentPageViewer
    CanGoToNextPage="{Binding NextPageAvailable, Mode=OneWayToSource}" ...>

これが許可されていれば完璧です。FlowDocumentPageViewerのCanGoToNextPageプロパティが変更されるたびに、新しい値がViewModelのNextPageAvailableプロパティにプッシュダウンされます。

残念ながら、これはコンパイルされません。「CanGoToPreviousPage」プロパティは読み取り専用であり、マークアップから設定できないというエラーが表示されます。どうやら読み取り専用プロパティは、そのプロパティに関して読み取り専用のデータバインディングでさえも、いかなる種類のデータバインディングサポートしていません。

ViewModelのプロパティをDependencyPropertiesにして、OneWayバインディングを別の方法で作成することもできますが、関心の分離違反に夢中ではありません(ViewModelは、MVVMデータバインディングが回避するはずのビューへの参照が必要になるでしょう) )。

FlowDocumentPageViewerはCanGoToNextPageChangedイベントを公開していません。また、DependencyPropertyから変更通知を取得するための適切な方法がわからないため、バインドする別のDependencyPropertyを作成する必要があります。

ビューの読み取り専用プロパティの変更をViewModelに通知し続けるにはどうすればよいですか?

回答:


151

はい、私は過去にActualWidthActualHeightプロパティを使用してこれを行いました。どちらも読み取り専用です。ObservedWidthObservedHeight添付プロパティを持つ添付ビヘイビアを作成しました。またObserve、最初の接続を行うために使用されるプロパティもあります。使用法は次のようになります。

<UserControl ...
    SizeObserver.Observe="True"
    SizeObserver.ObservedWidth="{Binding Width, Mode=OneWayToSource}"
    SizeObserver.ObservedHeight="{Binding Height, Mode=OneWayToSource}"

だから、ビューモデルを持っているWidthHeightと常に同期されているプロパティObservedWidthObservedHeight添付プロパティ。Observeプロパティは、単純に取り付けるSizeChangedのイベントFrameworkElement。ハンドルで、そのObservedWidthおよびObservedHeightプロパティを更新します。エルゴ、ビューモデルのWidthand Heightは常にActualWidthandのandと同期してActualHeightUserControlます。

おそらく完全な解決策ではないかもしれませんが(私は同意します-読み取り専用DP OneWayToSourceバインディングサポートする必要があります)、それは機能し、MVVMパターンを維持します。明らかに、ObservedWidthおよびObservedHeightDPは読み取り専用ではありません

更新:上記の機能を実装するコードは次のとおりです。

public static class SizeObserver
{
    public static readonly DependencyProperty ObserveProperty = DependencyProperty.RegisterAttached(
        "Observe",
        typeof(bool),
        typeof(SizeObserver),
        new FrameworkPropertyMetadata(OnObserveChanged));

    public static readonly DependencyProperty ObservedWidthProperty = DependencyProperty.RegisterAttached(
        "ObservedWidth",
        typeof(double),
        typeof(SizeObserver));

    public static readonly DependencyProperty ObservedHeightProperty = DependencyProperty.RegisterAttached(
        "ObservedHeight",
        typeof(double),
        typeof(SizeObserver));

    public static bool GetObserve(FrameworkElement frameworkElement)
    {
        frameworkElement.AssertNotNull("frameworkElement");
        return (bool)frameworkElement.GetValue(ObserveProperty);
    }

    public static void SetObserve(FrameworkElement frameworkElement, bool observe)
    {
        frameworkElement.AssertNotNull("frameworkElement");
        frameworkElement.SetValue(ObserveProperty, observe);
    }

    public static double GetObservedWidth(FrameworkElement frameworkElement)
    {
        frameworkElement.AssertNotNull("frameworkElement");
        return (double)frameworkElement.GetValue(ObservedWidthProperty);
    }

    public static void SetObservedWidth(FrameworkElement frameworkElement, double observedWidth)
    {
        frameworkElement.AssertNotNull("frameworkElement");
        frameworkElement.SetValue(ObservedWidthProperty, observedWidth);
    }

    public static double GetObservedHeight(FrameworkElement frameworkElement)
    {
        frameworkElement.AssertNotNull("frameworkElement");
        return (double)frameworkElement.GetValue(ObservedHeightProperty);
    }

    public static void SetObservedHeight(FrameworkElement frameworkElement, double observedHeight)
    {
        frameworkElement.AssertNotNull("frameworkElement");
        frameworkElement.SetValue(ObservedHeightProperty, observedHeight);
    }

    private static void OnObserveChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
        var frameworkElement = (FrameworkElement)dependencyObject;

        if ((bool)e.NewValue)
        {
            frameworkElement.SizeChanged += OnFrameworkElementSizeChanged;
            UpdateObservedSizesForFrameworkElement(frameworkElement);
        }
        else
        {
            frameworkElement.SizeChanged -= OnFrameworkElementSizeChanged;
        }
    }

    private static void OnFrameworkElementSizeChanged(object sender, SizeChangedEventArgs e)
    {
        UpdateObservedSizesForFrameworkElement((FrameworkElement)sender);
    }

    private static void UpdateObservedSizesForFrameworkElement(FrameworkElement frameworkElement)
    {
        // WPF 4.0 onwards
        frameworkElement.SetCurrentValue(ObservedWidthProperty, frameworkElement.ActualWidth);
        frameworkElement.SetCurrentValue(ObservedHeightProperty, frameworkElement.ActualHeight);

        // WPF 3.5 and prior
        ////SetObservedWidth(frameworkElement, frameworkElement.ActualWidth);
        ////SetObservedHeight(frameworkElement, frameworkElement.ActualHeight);
    }
}

2
Observeを必要とせずに、プロパティを自動的にアタッチするためのいくつかのトリックを実行できるでしょうか。しかし、これは素晴らしい解決策のように見えます。ありがとう!
ジョーホワイト

1
ケント、ありがとう。この「SizeObserver」クラスのコードサンプルを以下に投稿しました。
スコットウィットロック

52
この感情への+1:「読み取り専用DPはOneWayToSourceバインディングをサポートする必要があります」
Tristan

3
おそらくSize、HeigthとWidthを組み合わせて1つのプロパティのみを作成する方が良いでしょう。約 コードを50%削減。
ジェラール

1
@Gerard:にはActualSizeプロパティがないため、機能しませんFrameworkElement。添付プロパティを直接バインドする場合はActualWidthActualHeightそれぞれにバインドされる2つのプロパティを作成する必要があります。
dotNET

58

私は、ActualWidthとActualHeightだけでなく、少なくとも読み取りモードでバインドできるすべてのデータでも機能するユニバーサルソリューションを使用しています。

ViewportWidthとViewportHeightがビューモデルのプロパティである場合、マークアップは次のようになります。

<Canvas>
    <u:DataPiping.DataPipes>
         <u:DataPipeCollection>
             <u:DataPipe Source="{Binding RelativeSource={RelativeSource AncestorType={x:Type Canvas}}, Path=ActualWidth}"
                         Target="{Binding Path=ViewportWidth, Mode=OneWayToSource}"/>
             <u:DataPipe Source="{Binding RelativeSource={RelativeSource AncestorType={x:Type Canvas}}, Path=ActualHeight}"
                         Target="{Binding Path=ViewportHeight, Mode=OneWayToSource}"/>
          </u:DataPipeCollection>
     </u:DataPiping.DataPipes>
<Canvas>

これがカスタム要素のソースコードです

public class DataPiping
{
    #region DataPipes (Attached DependencyProperty)

    public static readonly DependencyProperty DataPipesProperty =
        DependencyProperty.RegisterAttached("DataPipes",
        typeof(DataPipeCollection),
        typeof(DataPiping),
        new UIPropertyMetadata(null));

    public static void SetDataPipes(DependencyObject o, DataPipeCollection value)
    {
        o.SetValue(DataPipesProperty, value);
    }

    public static DataPipeCollection GetDataPipes(DependencyObject o)
    {
        return (DataPipeCollection)o.GetValue(DataPipesProperty);
    }

    #endregion
}

public class DataPipeCollection : FreezableCollection<DataPipe>
{

}

public class DataPipe : Freezable
{
    #region Source (DependencyProperty)

    public object Source
    {
        get { return (object)GetValue(SourceProperty); }
        set { SetValue(SourceProperty, value); }
    }
    public static readonly DependencyProperty SourceProperty =
        DependencyProperty.Register("Source", typeof(object), typeof(DataPipe),
        new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnSourceChanged)));

    private static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ((DataPipe)d).OnSourceChanged(e);
    }

    protected virtual void OnSourceChanged(DependencyPropertyChangedEventArgs e)
    {
        Target = e.NewValue;
    }

    #endregion

    #region Target (DependencyProperty)

    public object Target
    {
        get { return (object)GetValue(TargetProperty); }
        set { SetValue(TargetProperty, value); }
    }
    public static readonly DependencyProperty TargetProperty =
        DependencyProperty.Register("Target", typeof(object), typeof(DataPipe),
        new FrameworkPropertyMetadata(null));

    #endregion

    protected override Freezable CreateInstanceCore()
    {
        return new DataPipe();
    }
}

(user543564からの回答による):これは回答ではなくDmitryへのコメントです。私はあなたのソリューションを使用しましたが、うまくいきました。さまざまな場所で一般的に使用できる素晴らしいユニバーサルソリューション。これを使用して、UI要素のプロパティ(ActualHeightとActualWidth)をビューモデルにプッシュしました。
マークグラベル

2
ありがとう!これは、通常のget onlyプロパティにバインドするのに役立ちました。残念ながら、このプロパティはINotifyPropertyChangedイベントを発行しませんでした。DataPipeバインディングに名前を割り当て、コントロールの変更されたイベントに以下を追加することで、これを解決しました。BindingOperations.GetBindingExpressionBase(bindingName、DataPipe.SourceProperty).UpdateTarget();
chilltemp 2011年

3
この解決策は私にとってうまくいきました。私の唯一の微調整は、TargetProperty DependencyPropertyのFrameworkPropertyMetadataに対してBindsTwoWayByDefaultをtrueに設定することでした。
Hasani Blackwell

1
このソリューションの唯一の不満Targetは、外部から変更してはならないにもかかわらずプロパティを書き込み可能にする必要があるため、クリーンなカプセル化が解除されることです:-/
またはMapper

コードのコピーアンドペーストよりもNuGetパッケージを好む人のために:オープンソースのJungleControlsライブラリにDataPipeを追加しました。DataPipeのドキュメントを参照してください。
RobertVažan2014

21

他の誰かが興味を持っているなら、私はケントの解の近似をここにコード化しました:

class SizeObserver
{
    #region " Observe "

    public static bool GetObserve(FrameworkElement elem)
    {
        return (bool)elem.GetValue(ObserveProperty);
    }

    public static void SetObserve(
      FrameworkElement elem, bool value)
    {
        elem.SetValue(ObserveProperty, value);
    }

    public static readonly DependencyProperty ObserveProperty =
        DependencyProperty.RegisterAttached("Observe", typeof(bool), typeof(SizeObserver),
        new UIPropertyMetadata(false, OnObserveChanged));

    static void OnObserveChanged(
      DependencyObject depObj, DependencyPropertyChangedEventArgs e)
    {
        FrameworkElement elem = depObj as FrameworkElement;
        if (elem == null)
            return;

        if (e.NewValue is bool == false)
            return;

        if ((bool)e.NewValue)
            elem.SizeChanged += OnSizeChanged;
        else
            elem.SizeChanged -= OnSizeChanged;
    }

    static void OnSizeChanged(object sender, RoutedEventArgs e)
    {
        if (!Object.ReferenceEquals(sender, e.OriginalSource))
            return;

        FrameworkElement elem = e.OriginalSource as FrameworkElement;
        if (elem != null)
        {
            SetObservedWidth(elem, elem.ActualWidth);
            SetObservedHeight(elem, elem.ActualHeight);
        }
    }

    #endregion

    #region " ObservedWidth "

    public static double GetObservedWidth(DependencyObject obj)
    {
        return (double)obj.GetValue(ObservedWidthProperty);
    }

    public static void SetObservedWidth(DependencyObject obj, double value)
    {
        obj.SetValue(ObservedWidthProperty, value);
    }

    // Using a DependencyProperty as the backing store for ObservedWidth.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ObservedWidthProperty =
        DependencyProperty.RegisterAttached("ObservedWidth", typeof(double), typeof(SizeObserver), new UIPropertyMetadata(0.0));

    #endregion

    #region " ObservedHeight "

    public static double GetObservedHeight(DependencyObject obj)
    {
        return (double)obj.GetValue(ObservedHeightProperty);
    }

    public static void SetObservedHeight(DependencyObject obj, double value)
    {
        obj.SetValue(ObservedHeightProperty, value);
    }

    // Using a DependencyProperty as the backing store for ObservedHeight.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ObservedHeightProperty =
        DependencyProperty.RegisterAttached("ObservedHeight", typeof(double), typeof(SizeObserver), new UIPropertyMetadata(0.0));

    #endregion
}

アプリでお気軽にご利用ください。それはうまくいきます。(ケントありがとう!)


10

これは私がここでブログに書いたこの「バグ」の別の解決策です:
ReadOnly DependencyプロパティのOneWayToSourceバインディング

これは、リスナーとミラーという2つの依存関係プロパティを使用して機能します。リスナーはOneWayをTargetPropertyにバインドし、PropertyChangedCallbackで、Bindingで指定されたものにOneWayToSourceにバインドされているMirrorプロパティを更新します。私はそれを呼び出しPushBinding、これはこのような読み取り専用の依存関係プロパティに設定できます

<TextBlock Name="myTextBlock"
           Background="LightBlue">
    <pb:PushBindingManager.PushBindings>
        <pb:PushBinding TargetProperty="ActualHeight" Path="Height"/>
        <pb:PushBinding TargetProperty="ActualWidth" Path="Width"/>
    </pb:PushBindingManager.PushBindings>
</TextBlock>

こちらからデモプロジェクトをダウンロードしてください
ソースコードと短いサンプルの使用法が含まれています。実装の詳細に興味がある場合は、私のWPFブログにアクセスしください。

.NET 4.0以降、OneWayToSourceバインディングが値を更新した後、Sourceから値を読み取るため、.NET 4.0以降はビルトインサポートから遠く離れています。


Stack Overflowの回答は完全に自己完結型である必要があります。オプションの外部参照へのリンクを含めることは問題ありませんが、回答に必要なすべてのコードを回答自体に含める必要があります。他のWebサイトにアクセスせずに使用できるように、質問を更新してください。
Peter Duniho

4

私はドミトリー・タシキノフのソリューションが好きです!しかし、デザインモードでVSがクラッシュしました。これが、OnSourceChangedメソッドに行を追加した理由です。

    プライベート静的void OnSourceChanged(DependencyObject d、DependencyPropertyChangedEventArgs e)
    {
        if(!((bool)DesignerProperties.IsInDesignModeProperty.GetMetadata(typeof(DependencyObject))。DefaultValue))
            ((DataPipe)d).OnSourceChanged(e);
    }

0

私はそれが少し簡単にできると思います:

xaml:

behavior:ReadOnlyPropertyToModelBindingBehavior.ReadOnlyDependencyProperty="{Binding ActualWidth, RelativeSource={RelativeSource Self}}"
behavior:ReadOnlyPropertyToModelBindingBehavior.ModelProperty="{Binding MyViewModelProperty}"

cs:

public class ReadOnlyPropertyToModelBindingBehavior
{
  public static readonly DependencyProperty ReadOnlyDependencyPropertyProperty = DependencyProperty.RegisterAttached(
     "ReadOnlyDependencyProperty", 
     typeof(object), 
     typeof(ReadOnlyPropertyToModelBindingBehavior),
     new PropertyMetadata(OnReadOnlyDependencyPropertyPropertyChanged));

  public static void SetReadOnlyDependencyProperty(DependencyObject element, object value)
  {
     element.SetValue(ReadOnlyDependencyPropertyProperty, value);
  }

  public static object GetReadOnlyDependencyProperty(DependencyObject element)
  {
     return element.GetValue(ReadOnlyDependencyPropertyProperty);
  }

  private static void OnReadOnlyDependencyPropertyPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
  {
     SetModelProperty(obj, e.NewValue);
  }


  public static readonly DependencyProperty ModelPropertyProperty = DependencyProperty.RegisterAttached(
     "ModelProperty", 
     typeof(object), 
     typeof(ReadOnlyPropertyToModelBindingBehavior), 
     new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

  public static void SetModelProperty(DependencyObject element, object value)
  {
     element.SetValue(ModelPropertyProperty, value);
  }

  public static object GetModelProperty(DependencyObject element)
  {
     return element.GetValue(ModelPropertyProperty);
  }
}

2
少し単純になるかもしれませんが、よく読んだ場合は、Elementでそのようなバインディングを1つだけ許可します。つまり、このアプローチでは、ActualWidth ActualHeightの両方をバインドすることはできない思います。それらの1つだけ。
ケツァルコアトル2018年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.