Mendorong kembali properti GUI hanya-baca ke ViewModel

124

Saya ingin menulis ViewModel yang selalu mengetahui keadaan saat ini dari beberapa properti ketergantungan hanya-baca dari View.

Secara khusus, GUI saya berisi FlowDocumentPageViewer, yang menampilkan satu halaman dalam satu waktu dari FlowDocument. FlowDocumentPageViewer memperlihatkan dua properti dependensi hanya-baca yang disebut CanGoToPreviousPage dan CanGoToNextPage. Saya ingin ViewModel saya selalu mengetahui nilai dari dua properti View ini.

Saya pikir saya bisa melakukan ini dengan penyatuan data OneWayToSource:

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

Jika ini diizinkan, itu akan sempurna: setiap kali properti CanGoToNextPage FlowDocumentPageViewer berubah, nilai baru akan didorong ke properti NextPageAvailable ViewModel, yang persis seperti yang saya inginkan.

Sayangnya, ini tidak dapat dikompilasi: Saya mendapatkan pesan kesalahan yang mengatakan bahwa properti 'CanGoToPreviousPage' bersifat hanya-baca dan tidak dapat disetel dari markup. Tampaknya properti hanya-baca tidak mendukung segala jenis penyatuan data, bahkan penyatuan data yang hanya-baca sehubungan dengan properti itu.

Saya dapat membuat properti ViewModel saya menjadi DependencyProperties, dan membuat pengikatan OneWay ke arah lain, tetapi saya tidak tergila-gila dengan pelanggaran pemisahan kepentingan (ViewModel akan memerlukan referensi ke Tampilan, yang seharusnya dihindari oleh penyatuan data MVVM ).

FlowDocumentPageViewer tidak mengekspos peristiwa CanGoToNextPageChanged, dan saya tidak tahu cara yang baik untuk mendapatkan pemberitahuan perubahan dari DependencyProperty, selain membuat DependencyProperty lain untuk mengikatnya, yang tampaknya berlebihan di sini.

Bagaimana cara agar ViewModel saya tetap terinformasi tentang perubahan pada properti tampilan hanya-baca?

Joe White
sumber

Jawaban:

152

Ya, saya pernah melakukan ini dengan properti ActualWidthdan ActualHeight, yang keduanya hanya dapat dibaca. Saya membuat perilaku terlampir yang memiliki ObservedWidthdan ObservedHeightmelampirkan properti. Ia juga memiliki Observeproperti yang digunakan untuk melakukan hook-up awal. Penggunaannya terlihat seperti ini:

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

Jadi model tampilan memiliki Widthdan Heightproperti yang selalu sinkron dengan ObservedWidthdanObservedHeight melekat sifat. The Observeproperti hanya menempel pada SizeChangedacara dari FrameworkElement. Di pegangan, itu memperbarui ObservedWidthdan ObservedHeightpropertinya. Ergo, yang Widthdan Heightdari model tampilan selalu sinkron dengan ActualWidthdan ActualHeightdari UserControl.

Mungkin bukan solusi yang tepat (saya setuju - DP hanya-baca harus mendukung OneWayToSourcebinding), tetapi berfungsi dan menjunjung tinggi pola MVVM. Jelas, ObservedWidthdan ObservedHeightDP tidak hanya bisa dibaca.

PEMBARUAN: berikut ini kode yang menerapkan fungsionalitas yang dijelaskan di atas:

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);
    }
}
Kent Boogaart
sumber
2
Saya ingin tahu apakah Anda bisa melakukan tipu daya untuk melampirkan properti secara otomatis, tanpa perlu Amati. Tapi ini sepertinya solusi yang bagus. Terima kasih!
Joe White
1
Terima kasih Kent. Saya memposting contoh kode di bawah ini untuk kelas "SizeObserver" ini.
Scott Whitlock
52
+1 untuk sentimen ini: "DP hanya-baca harus mendukung pengikatan OneWayToSource"
Tristan
3
Mungkin lebih baik membuat hanya satu Sizeproperti, menggabungkan Tinggi dan Lebar. Approx. 50% lebih sedikit kode.
Gerard
1
@ Gerard: Itu tidak akan berhasil karena tidak ada ActualSizeproperti di dalamnya FrameworkElement. Jika Anda ingin mengikat langsung properti terlampir, Anda harus membuat dua properti yang akan diikat ActualWidthdan ActualHeightmasing - masing.
dotNET
59

Saya menggunakan solusi universal yang tidak hanya berfungsi dengan ActualWidth dan ActualHeight, tetapi juga dengan data apa pun yang dapat Anda ikat setidaknya dalam mode membaca.

Markupnya terlihat seperti ini, asalkan ViewportWidth dan ViewportHeight adalah properti model tampilan

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

Berikut ini kode sumber untuk elemen khusus

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();
    }
}
Dmitry Tashkinov
sumber
(melalui jawaban dari pengguna543564): Ini bukan jawaban tetapi komentar untuk Dmitry - Saya menggunakan solusi Anda dan berfungsi dengan baik. Solusi universal yang bagus yang dapat digunakan secara umum di berbagai tempat. Saya menggunakannya untuk mendorong beberapa properti elemen ui (ActualHeight dan ActualWidth) ke viewmodel saya.
Marc Gravell
2
Terima kasih! Ini membantu saya terikat pada properti biasa saja. Sayangnya, properti tidak memublikasikan peristiwa INotifyPropertyChanged. Saya memecahkan ini dengan menetapkan nama ke pengikatan DataPipe dan menambahkan yang berikut ini ke acara yang diubah kontrol: BindingOperations.GetBindingExpressionBase (bindingName, DataPipe.SourceProperty) .UpdateTarget ();
chilltemp
3
Solusi ini bekerja dengan baik untuk saya. Satu-satunya perubahan saya adalah menyetel BindsTwoWayByDefault ke true untuk FrameworkPropertyMetadata pada TargetProperty DependencyProperty.
Hasani Blackwell
1
Satu-satunya keluhan tentang solusi ini tampaknya adalah bahwa ia merusak enkapsulasi yang bersih, karena Targetproperti tersebut harus dibuat dapat ditulisi meskipun tidak boleh diubah dari luar: - /
ATAU Mapper
Bagi mereka yang lebih suka paket NuGet daripada copy-n-paste kode: Saya telah menambahkan DataPipe ke perpustakaan JungleControls opensource saya. Lihat dokumentasi DataPipe .
Robert Važan
21

Jika ada orang lain yang tertarik, saya membuat perkiraan solusi Kent di sini:

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
}

Jangan ragu untuk menggunakannya di aplikasi Anda. Ini bekerja dengan baik. (Terima kasih Kent!)

Scott Whitlock
sumber
10

Berikut adalah solusi lain untuk "bug" ini yang saya tulis di blog:
Pengikatan OneWayToSource untuk Properti Ketergantungan ReadOnly

Ia bekerja dengan menggunakan dua Properti Ketergantungan, Pendengar dan Cermin. Pemroses mengikat OneWay ke TargetProperty dan di PropertyChangedCallback, pemroses memperbarui properti Mirror yang mengikat OneWayToSource ke apa pun yang ditentukan di Binding. Saya menyebutnya PushBindingdan dapat disetel pada Properti Ketergantungan hanya-baca seperti ini

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

Unduh Proyek Demo Disini .
Ini berisi kode sumber dan penggunaan sampel singkat, atau kunjungi blog WPF saya jika Anda tertarik dengan detail implementasi.

Satu catatan terakhir, sejak .NET 4.0 kami bahkan lebih jauh dari dukungan bawaan untuk ini, karena Pengikatan OneWayToSource membaca nilai kembali dari Sumber setelah memperbaruinya

Fredrik Hedblad
sumber
Jawaban di Stack Overflow harus berdiri sendiri. Tidak masalah untuk menyertakan tautan ke referensi eksternal opsional, tetapi semua kode yang diperlukan untuk jawaban harus disertakan dalam jawaban itu sendiri. Harap perbarui pertanyaan Anda agar dapat digunakan tanpa mengunjungi situs web lain.
Peter Duniho
4

Saya suka solusi Dmitry Tashkinov! Namun itu menabrak VS saya dalam mode desain. Itu sebabnya saya menambahkan baris ke metode OnSourceChanged:

    private static void OnSourceChanged (DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (! ((bool) DesignerProperties.IsInDesignModeProperty.GetMetadata (typeof (DependencyObject)). DefaultValue))
            ((DataPipe) d) .OnSourceChanged (e);
    }
Dariusz Wasacz
sumber
0

Saya pikir itu bisa dilakukan sedikit lebih sederhana:

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);
  }
}
eriksmith200
sumber
2
Mungkin sedikit lebih sederhana, tetapi jika saya membacanya dengan baik, ini hanya memungkinkan satu pengikatan pada Elemen tersebut. Maksud saya, menurut saya dengan pendekatan ini, Anda tidak akan dapat mengikat ActualWidth dan ActualHeight. Salah satunya.
quetzalcoatl