Đẩy các thuộc tính GUI chỉ đọc trở lại vào ViewModel


124

Tôi muốn viết một ViewModel luôn biết trạng thái hiện tại của một số thuộc tính phụ thuộc chỉ đọc từ Chế độ xem.

Cụ thể, GUI của tôi chứa FlowDocumentPageViewer, hiển thị một trang mỗi lần từ FlowDocument. FlowDocumentPageViewer hiển thị hai thuộc tính phụ thuộc chỉ đọc được gọi là CanGoToPreinglyPage và CanGoToNextPage. Tôi muốn ViewModel của tôi luôn biết các giá trị của hai thuộc tính View này.

Tôi hình dung tôi có thể làm điều này với cơ sở dữ liệu OneWayToSource:

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

Nếu điều này được cho phép, nó sẽ hoàn hảo: bất cứ khi nào thuộc tính CanGoToNextPage của FlowDocumentPageViewer thay đổi, giá trị mới sẽ được đẩy xuống thuộc tính NextPageAv Available của ViewModel, đó chính xác là những gì tôi muốn.

Thật không may, điều này không được biên dịch: Tôi gặp lỗi khi nói thuộc tính 'CanGoToPreinglyPage' là chỉ đọc và không thể được đặt từ đánh dấu. Các thuộc tính chỉ đọc rõ ràng không hỗ trợ bất kỳ loại dữ liệu nào, thậm chí cả các cơ sở dữ liệu chỉ đọc đối với thuộc tính đó.

Tôi có thể biến các thuộc tính của ViewModel của mình thành DependencyProperations và tạo ràng buộc OneWay theo cách khác, nhưng tôi không điên về vi phạm phân tách mối quan tâm (ViewModel sẽ cần tham chiếu đến Chế độ xem dữ liệu MVVM. ).

FlowDocumentPageViewer không phơi bày sự kiện CanGoToNextPageChanged và tôi không biết cách nào tốt để nhận thông báo thay đổi từ DependencyProperty, không tạo ra một DependencyProperty khác để liên kết nó, dường như quá mức cần thiết ở đây.

Làm cách nào tôi có thể thông báo cho ViewModel về các thay đổi đối với các thuộc tính chỉ đọc của chế độ xem?

Câu trả lời:


151

Có, tôi đã làm điều này trong quá khứ với các thuộc tính ActualWidthActualHeightcả hai đều chỉ đọc. Tôi đã tạo ra một hành vi đính kèm có ObservedWidthObservedHeightthuộc tính đính kèm. Nó cũng có một thuộc Observetính được sử dụng để thực hiện hook-up ban đầu. Cách sử dụng trông như thế này:

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

Vì vậy, mô hình khung nhìn có WidthHeightcác thuộc tính luôn đồng bộ với các thuộc tính ObservedWidthvà được ObservedHeightđính kèm. Các Observetài sản chỉ đơn giản là gắn liền với SizeChangedsự kiện của FrameworkElement. Trong tay cầm, nó cập nhật ObservedWidthObservedHeightthuộc tính của nó . Ergo, WidthHeightmô hình khung nhìn luôn đồng bộ với ActualWidthActualHeightcủa UserControl.

Có lẽ không phải là giải pháp hoàn hảo (tôi đồng ý - các DP chỉ đọc nên hỗ trợ OneWayToSourcecác ràng buộc), nhưng nó hoạt động và nó duy trì mô hình MVVM. Rõ ràng, DP ObservedWidthkhông chỉ đọc.ObservedHeight

CẬP NHẬT: đây là mã thực hiện các chức năng được mô tả ở trên:

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
Tôi tự hỏi nếu bạn có thể làm một số thủ thuật để tự động đính kèm các thuộc tính, mà không cần phải quan sát. Nhưng điều này có vẻ như một giải pháp tốt. Cảm ơn!
Joe White

1
Cảm ơn Kent. Tôi đã đăng một mẫu mã bên dưới cho lớp "SizeObserver" này.
Scott Whitlock

52
+1 cho tình cảm này: "DP chỉ đọc nên hỗ trợ các ràng buộc OneWayToSource"
Tristan

3
Có lẽ thậm chí tốt hơn để tạo chỉ một thuộc Sizetính, kết hợp Heigth và Width. Xấp xỉ Mã ít hơn 50%.
Gerard

1
@Gerard: Điều đó sẽ không hoạt động vì không có ActualSizetài sản trong FrameworkElement. Nếu bạn muốn liên kết trực tiếp các thuộc tính đính kèm, bạn phải tạo hai thuộc tính để được ràng buộc ActualWidthActualHeighttương ứng.
dotNET

58

Tôi sử dụng một giải pháp phổ quát không chỉ hoạt động với ActualWidth và ActualHeight, mà còn với bất kỳ dữ liệu nào bạn có thể liên kết với ít nhất là trong chế độ đọc.

Đánh dấu trông như thế này, được cung cấp ViewportWidth và ViewportHeight là các thuộc tính của mô hình khung nhìn

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

Đây là mã nguồn cho các yếu tố tùy chỉnh

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();
    }
}

(thông qua câu trả lời từ user543564): Đây không phải là câu trả lời mà là nhận xét cho Dmitry - Tôi đã sử dụng giải pháp của bạn và nó hoạt động rất tốt. Giải pháp phổ quát tốt đẹp có thể được sử dụng rộng rãi ở những nơi khác nhau. Tôi đã sử dụng nó để đẩy một số thuộc tính phần tử ui (ActualHeight và ActualWidth) vào viewmodel của tôi.
Marc Gravell

2
Cảm ơn! Điều này giúp tôi liên kết với một tài sản chỉ nhận được bình thường. Thật không may, tài sản đã không xuất bản các sự kiện INotifyPropertyChanged. Tôi đã giải quyết điều này bằng cách gán tên cho liên kết DataPipe và thêm các điều sau vào sự kiện đã thay đổi điều khiển: BindingOperations.GetBindingExpressionBase (bindName, DataPipe.SourceProperty) .UpdateTarget ();
chilltemp

3
Giải pháp này đã làm việc tốt cho tôi. Điều chỉnh duy nhất của tôi là đặt BindsTwoWayByDefault thành true cho FrameworkPropertyMetadata trên TargetProperty DependencyProperty.
Hasani Blackwell

1
Nắm bắt duy nhất về giải pháp này dường như là nó phá vỡ đóng gói sạch, vì Targettài sản phải được ghi lại mặc dù không được thay đổi từ bên ngoài: - /
HOẶC Mapper

Đối với những người thích gói NuGet hơn là sao chép mã: tôi đã thêm DataPipe vào thư viện JungleControls mã nguồn mở của mình. Xem tài liệu DataPipe .
Robert Važan

21

Nếu có ai quan tâm, tôi đã mã hóa gần đúng giải pháp của Kent ở đây:

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
}

Hãy sử dụng nó trong ứng dụng của bạn. Nó hoạt động tốt. (Cảm ơn Kent!)


10

Đây là một giải pháp khác cho "lỗi" mà tôi đã viết ở đây:
OneWayToSource Binding cho ReadOnly Dependency property

Nó hoạt động bằng cách sử dụng hai thuộc tính phụ thuộc, Listener và Mirror. Listener bị ràng buộc OneWay với TargetProperty và trong PropertyChangedCallback, nó cập nhật thuộc tính Mirror được ràng buộc OneWayToSource thành bất cứ điều gì được chỉ định trong Binding. Tôi gọi nó PushBindingvà nó có thể được đặt trên bất kỳ Thuộc tính phụ thuộc chỉ đọc như thế này

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

Tải về Dự án Demo tại đây .
Nó chứa mã nguồn và sử dụng mẫu ngắn hoặc truy cập blog WPF của tôi nếu bạn quan tâm đến các chi tiết triển khai.

Một lưu ý cuối cùng, vì .NET 4.0, chúng tôi thậm chí còn không hỗ trợ tích hợp cho việc này, vì Liên kết OneWayToSource đọc lại giá trị từ Nguồn sau khi đã cập nhật nó


Câu trả lời trên Stack Overflow nên hoàn toàn khép kín. Sẽ tốt hơn nếu bao gồm một liên kết đến các tham chiếu bên ngoài tùy chọn, nhưng tất cả các mã cần thiết cho câu trả lời nên được bao gồm trong chính câu trả lời. Vui lòng cập nhật câu hỏi của bạn để có thể sử dụng nó mà không cần truy cập bất kỳ trang web nào khác.
Peter Duniho

4

Tôi thích giải pháp của Dmitry Tashkinov! Tuy nhiên, nó bị sập VS của tôi trong chế độ thiết kế. Đó là lý do tại sao tôi đã thêm một dòng vào phương thức OnSourceChanged:

    khoảng trống tĩnh riêng OnSourceChanged (DependencyObject d, DependencyPropertyChangedEventArss e)
    {
        if (! ((bool) DesignerProperIES.IsInDesignModeProperty.GetMetadata (typeof (DependencyObject)). DefaultValue))
            ((DataPipe) d) .OnSourceChanged (e);
    }

0

Tôi nghĩ rằng nó có thể được thực hiện đơn giản hơn một chút:

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
Có thể đơn giản hơn một chút, nhưng nếu tôi đọc tốt, nó chỉ cho phép một ràng buộc như vậy trên Element. Ý tôi là, tôi nghĩ rằng với cách tiếp cận này, bạn sẽ không thể ràng buộc cả ActualWidth ActualHeight. Chỉ là một trong số họ.
quetzalcoatl
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.