2009-07-05 15 views
106

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.Đẩy các thuộc tính GUI chỉ đọc trở lại vào ViewModel

Cụ thể, GUI của tôi chứa FlowDocumentPageViewer, hiển thị một trang tại một thời điểm từ FlowDocument. FlowDocumentPageViewer cho thấy hai thuộc tính phụ thuộc chỉ đọc có tên là CanGoToPreviousPage và CanGoToNextPage. Tôi muốn ViewModel của mình luôn biết giá trị của hai thuộc tính Chế độ xem này.

I figured tôi có thể làm điều này với một liên kết dữ liệu OneWayToSource:

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

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

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

Tôi có thể biến thuộc tính của ViewModel thành DependencyProperties và tạo liên kết OneWay theo cách khác, nhưng tôi không điên về vi phạm ly thân (ViewModel sẽ cần tham chiếu đến Chế độ xem, dữ liệu MVVM có nghĩa vụ phải tránh).

FlowDocumentPageViewer không hiển thị sự kiện CanGoToNextPageChanged và tôi không biết cách nào để nhận thông báo thay đổi từ DependencyProperty, ngắn tạo DependencyProperty khác để liên kết nó, có vẻ như quá mức ở đây.

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

Trả lời

129

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

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

Vì vậy, mô hình xem có WidthHeight tài sản mà luôn đồng bộ với ObservedWidthObservedHeight tài sản gắn liền. Thuộc tính Observe chỉ cần gắn vào sự kiện SizeChanged của số FrameworkElement. Trong tay cầm, nó cập nhật các thuộc tính ObservedWidthObservedHeight của nó. Ergo, WidthHeight của kiểu xem luôn đồng bộ với ActualWidthActualHeight của UserControl.

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

UPDATE: đây là mã mà thực hiện các chức năng 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ể thực hiện một số thủ thuật để tự động đính kèm các thuộc tính mà không cần Quan sát. Nhưng điều này trông giống như một giải pháp tốt. Cảm ơn! –

+1

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

+42

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

20

Nếu bất cứ ai khác quan tâm, tôi được mã hóa lên một xấp xỉ của giải pháp 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!)

49

Tôi sử dụng 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 ở chế độ đọc.

Các đánh dấu trông như thế này, với điều kiện ViewportWidth và ViewportHeight là tài sản của mô hình xem

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

Dưới đâ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(); 
    } 
} 
+0

(thông qua câu trả lời từ người dùng543564): Đây không phải là câu trả lời nhưng nhận xét về 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 có thể được sử dụng chung ở 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. –

+2

Cảm ơn! Điều này đã giúp tôi liên kết với một tài sản bình thường. Rất tiếc, thuộc tính đã không xuất bản các sự kiện INotifyPropertyChanged. Tôi đã giải quyết vấn đề này bằng cách gán một tên cho ràng buộc DataPipe và thêm phần sau vào sự kiện đã thay đổi điều khiển: BindingOperations.GetBindingExpressionBase (bindingName, DataPipe.SourceProperty) .UpdateTarget(); – chilltemp

+3

Giải pháp này làm việc tốt cho tôi. Tinh chỉnh duy nhất của tôi là thiết lập BindsTwoWayByDefault thành true cho FrameworkPropertyMetadata trên TargetProperty DependencyProperty. –

9

Đây là một giải pháp khác để này " lỗi "mà tôi viết blog về đây:
OneWayToSource Binding for ReadOnly Dependency Property

Nó hoạt động bằng cách sử dụng t wo Dependency Properties, Listener và Mirror. Trình nghe bị ràng buộc OneWay với TargetProperty và trong PropertyChangedCallback nó cập nhật thuộc tính Mirror bị ràng buộc OneWayToSource vào bất kỳ thứ gì được chỉ định trong Ràng buộc. Tôi gọi nó là PushBinding và nó có thể được đặt trên bất kỳ read-only phụ thuộc tài sản 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> 

Download Demo Project Here.
Mã này chứa mã nguồn và sử dụng mẫu ngắn hoặc truy cập my WPF blog nếu bạn quan tâm đến 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 xa built-in hỗ trợ cho điều này, kể từ khi một OneWayToSource Binding reads the value back from the Source after it has updated it

4

Tôi thích giải pháp Dmitry Tashkinov của! Tuy nhiên nó bị hỏng 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 pháp OnSourceChanged:

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

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

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); 
    } 
} 
+0

Có thể đơn giản hơn một chút, nhưng nếu tôi đọc nó tốt, nó cho phép ** chỉ một ** liên kết 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ể liên kết cả ActualWidth ** và ** ActualHeight. Chỉ một trong số họ. – quetzalcoatl

Các vấn đề liên quan