Salt okunur GUI özelliklerini ViewModel'e geri gönderme


124

View'dan bazı salt okunur bağımlılık özelliklerinin mevcut durumunu her zaman bilen bir ViewModel yazmak istiyorum.

Özellikle, benim GUI, bir FlowDocument'den her seferinde bir sayfa görüntüleyen bir FlowDocumentPageViewer içerir. FlowDocumentPageViewer, CanGoToPreviousPage ve CanGoToNextPage adlı iki salt okunur bağımlılık özelliğini ortaya çıkarır. ViewModel'imin bu iki View özelliğinin değerlerini her zaman bilmesini istiyorum.

Bunu OneWayToSource veri bağlama ile yapabileceğimi düşündüm:

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

Buna izin verildiyse, mükemmel olurdu: FlowDocumentPageViewer'ın CanGoToNextPage özelliği her değiştiğinde, yeni değer ViewModel'in NextPageAvailable özelliğine itilirdi, ki bu tam olarak istediğim şeydi.

Ne yazık ki, bu derlenmiyor : 'CanGoToPreviousPage' özelliğinin salt okunur olduğunu ve işaretlemeden ayarlanamadığını söyleyen bir hata alıyorum . Görünüşe göre salt okunur özellikler, o özelliğe göre salt okunur olan veri bağlamayı bile, herhangi bir veri bağlamayı desteklemiyor .

ViewModel'in özelliklerini DependencyProperties yapabilir ve diğer tarafa giden bir OneWay bağlama yapabilirim, ancak endişelerin ayrılması ihlali konusunda deli değilim (ViewModel'in, MVVM veri tabanının kaçınması gereken View'a bir referansa ihtiyacı olacaktır. ).

FlowDocumentPageViewer bir CanGoToNextPageChanged olayını açığa çıkarmıyor ve bir DependencyProperty'den değişiklik bildirimleri almanın iyi bir yolunu bilmiyorum, buna bağlanmak için başka bir DependencyProperty oluşturmanın dışında, ki bu aşırıya kaçmış gibi görünüyor.

ViewModel'imi görünümün salt okunur özelliklerindeki değişikliklerden nasıl haberdar edebilirim?

Yanıtlar:


152

Evet, bunu geçmişte her ikisi de salt okunur olan ActualWidthve ActualHeightözellikleriyle yaptım . Özellikleri olan ObservedWidthve ObservedHeightekleyen ekli bir davranış oluşturdum . Aynı zamanda, Observeilk bağlanmayı yapmak için kullanılan bir özelliğe sahiptir . Kullanım şuna benzer:

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

Görünüm modeli vardır Yani Widthve Heightözellikleri ile her zaman senkronize olduğunu ObservedWidthve ObservedHeightekli özellikleri. ObserveMülkiyet basitçe bağlanır SizeChangeddurumunda FrameworkElement. Sapta, ObservedWidthve ObservedHeightözelliklerini günceller . Kaynayan Widthve Heightgörünüşüdür modeli ile her zaman senkronize edilir ActualWidthve ActualHeightbir UserControl.

Belki de değil mükemmel bir çözüm (Kabul ediyorum - salt okunur DPs gerektiğini destekleyen OneWayToSourcebağlamaları), ancak o inşaat ve MVVM desen onadı. Açıkçası, ObservedWidthve ObservedHeightDP'ler salt okunur değildir .

GÜNCELLEME: Yukarıda açıklanan işlevselliği uygulayan kod:

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
Gözlemlemeye gerek kalmadan özellikleri otomatik olarak eklemek için bazı hileler yapıp yapamayacağınızı merak ediyorum. Ama bu iyi bir çözüm gibi görünüyor. Teşekkürler!
Joe White

1
Teşekkürler Kent. Bu "SizeObserver" sınıfı için aşağıya bir kod örneği gönderdim.
Scott Whitlock

53
Bu düşünceye +1: "salt okunur DP'ler OneWayToSource bağlamalarını desteklemelidir"
Tristan

3
SizeYükseklik ve Genişliği birleştiren tek bir özellik oluşturmak belki daha da iyi . Yaklaşık. % 50 daha az kod.
Gerard

1
@Gerard: Bu işe yaramaz çünkü ActualSizeiçinde mülk yok FrameworkElement. Ekli özelliklerin doğrudan bağlanmasını istiyorsanız, sırasıyla ActualWidthve bağlanacak iki özellik oluşturmanız gerekir ActualHeight.
dotNET

59

Yalnızca ActualWidth ve ActualHeight ile değil, aynı zamanda en azından okuma modunda bağlayabileceğiniz herhangi bir veriyle de çalışan evrensel bir çözüm kullanıyorum.

ViewportWidth ve ViewportHeight'ın görünüm modelinin özellikleri olması koşuluyla, işaretleme şöyle görünür

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

İşte özel öğeler için kaynak kodu

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'ten gelen bir cevapla): Bu bir cevap değil, Dmitry'a bir yorum - çözümünüzü kullandım ve harika çalıştı. Farklı yerlerde jenerik olarak kullanılabilen güzel evrensel çözüm. Bazı ui öğesi özelliklerini (ActualHeight ve ActualWidth) görünüm modelime göndermek için kullandım.
Marc Gravell

2
Teşekkürler! Bu, normal bir yalnızca mülkiyete bağlanmama yardımcı oldu. Maalesef mülk INotifyPropertyChanged olaylarını yayınlamadı. Bunu, DataPipe bağlamasına bir ad atayarak ve aşağıdakileri değiştirilen kontrol olayına ekleyerek çözdüm: BindingOperations.GetBindingExpressionBase (bindingName, DataPipe.SourceProperty) .UpdateTarget ();
chilltemp

3
Bu çözüm benim için iyi çalıştı. Tek ince ayarım, TargetProperty DependencyProperty üzerindeki FrameworkPropertyMetadata için BindsTwoWayByDefault'u true olarak ayarlamaktı.
Hasani Blackwell

1
Bu çözümle ilgili tek sıkıntı, Targetözelliğin dışarıdan değiştirilmemesi gerekmesine rağmen yazılabilir hale getirilmesi gerektiğinden temiz kapsüllemeyi bozması gibi görünüyor : - /
VEYA Eşleyici

Kodu kopyala-yapıştır yerine NuGet paketini tercih edenler için: Açık kaynaklı JungleControls kitaplığıma DataPipe ekledim. DataPipe belgelerine bakın .
Robert Važan

21

İlgilenen başka biri varsa, buraya Kent'in çözümünün bir tahminini kodladım:

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
}

Uygulamalarınızda kullanmaktan çekinmeyin. İyi çalışıyor. (Teşekkürler Kent!)


10

Burada blog yazdığım bu "hata" için başka bir çözüm:
ReadOnly Bağımlılık Özelliği için OneWayToSource Bağlama

Dinleyici ve Ayna olmak üzere iki Bağımlılık Özelliği kullanarak çalışır. Dinleyici, OneWay'i TargetProperty'ye bağlar ve PropertyChangedCallback'te, OneWayToSource'a bağlanan Mirror özelliğini, Binding'de belirtilenlere göre günceller. Ben çağırıyorum PushBindingve bunun gibi herhangi bir salt okunur Bağımlılık Özelliğinde ayarlanabilir

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

Demo Projesini Buradan İndirin .
Kaynak kodu ve kısa örnek kullanım içerir veya uygulama ayrıntılarıyla ilgileniyorsanız WPF blogumu ziyaret edin .

Son bir not, .NET 4.0'dan beri, bir OneWayToSource Binding değeri güncelledikten sonra Kaynaktan geri okuduğundan, bunun için yerleşik destekten daha da uzaktayız.


Stack Overflow'daki yanıtlar tamamen bağımsız olmalıdır. İsteğe bağlı harici referanslara bir bağlantı eklemekte sorun yoktur, ancak cevap için gereken tüm kod, cevabın kendisine dahil edilmelidir. Lütfen sorunuzu başka bir web sitesini ziyaret etmeden kullanılabilecek şekilde güncelleyin.
Peter Duniho

4

Dmitry Tashkinov'un çözümünü beğendim! Ancak VS'mi tasarım modunda çöktü. Bu yüzden OnSourceChanged yöntemine bir satır ekledim:

    private static void OnSourceChanged (DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        eğer (! ((bool) DesignerProperties.IsInDesignModeProperty.GetMetadata (typeof (DependencyObject)). DefaultValue))
            ((DataPipe) d) (e) .OnSourceChanged;
    }

0

Sanırım biraz daha basit yapılabilir:

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
Biraz daha basit olabilir, ama iyi okursanız, bu izin verir miyim tek Unsuru böyle bağlayıcı. Yani, bu yaklaşımla, sen ActualWidth hem bağlamak mümkün olmayacaktır düşünüyorum ve ActualHeight. Sadece biri.
quetzalcoatl
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.