읽기 전용 GUI 속성을 다시 ViewModel로 푸시


124

View에서 읽기 전용 종속성 속성의 현재 상태를 항상 알고있는 ViewModel을 작성하고 싶습니다.

특히 내 GUI에는 FlowDocument에서 한 번에 한 페이지를 표시하는 FlowDocumentPageViewer가 포함되어 있습니다. FlowDocumentPageViewer는 CanGoToPreviousPage 및 CanGoToNextPage라는 두 개의 읽기 전용 종속성 속성을 노출합니다. 내 ViewModel이 항상이 두 View 속성의 값을 알고 있기를 바랍니다.

OneWayToSource 데이터 바인딩으로 이것을 할 수 있다고 생각했습니다.

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

이것이 허용되면 완벽 할 것입니다. FlowDocumentPageViewer의 CanGoToNextPage 속성이 변경 될 때마다 새 값이 ViewModel의 NextPageAvailable 속성으로 푸시됩니다.

불행히도 이것은 컴파일되지 않습니다. 'CanGoToPreviousPage'속성이 읽기 전용이며 마크 업에서 설정할 수 없다는 오류가 발생 합니다. 분명히 읽기 전용 속성은 어떤 종류의 데이터 바인딩도 지원하지 않으며 해당 속성 과 관련하여 읽기 전용 인 데이터 바인딩도 지원하지 않습니다 .

내 ViewModel의 속성을 DependencyProperties로 만들고 OneWay 바인딩을 다른 방향으로 만들 수는 있지만 우려 사항 분리 위반에 대해 열광하지는 않습니다. ).

FlowDocumentPageViewer는 CanGoToNextPageChanged 이벤트를 노출하지 않으며 DependencyProperty에서 변경 알림을 가져 오는 좋은 방법을 모릅니다. 바인드 할 다른 DependencyProperty를 만드는 것보다 부족합니다.

뷰의 읽기 전용 속성에 대한 변경 사항을 ViewModel에 알리려면 어떻게해야합니까?

답변:


152

예, 저는 과거 에 읽기 전용 인 ActualWidthActualHeight속성을 사용하여이 작업을 수행했습니다 . 속성 이 ObservedWidth있고 ObservedHeight연결된 연결된 동작을 만들었습니다 . 또한 Observe초기 연결을 수행하는 데 사용되는 속성이 있습니다. 사용법은 다음과 같습니다.

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

뷰 모델이있다 그래서 WidthHeight등록 정보와 동기화 항상 그 ObservedWidthObservedHeight연결된 속성. Observe속성은 단순히 부착 SizeChanged의 경우 FrameworkElement. 핸들에서 ObservedWidthObservedHeight속성을 업데이트 합니다. 인체 공학적는 WidthHeight뷰 모델은 동기화 항상 ActualWidth하고 ActualHeightUserControl.

아마도 완벽한 솔루션은 아니지만 (동의합니다-읽기 전용 DP OneWayToSource 바인딩 지원 해야 함 ) 작동하며 MVVM 패턴을 유지합니다. 분명히, ObservedWidth그리고 ObservedHeight드프가되어 있지 읽기 전용입니다.

업데이트 : 위에 설명 된 기능을 구현하는 코드는 다음과 같습니다.

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 없이도 속성을 자동으로 연결하는 몇 가지 속임수를 사용할 수 있는지 궁금합니다. 그러나 이것은 좋은 해결책처럼 보입니다. 감사!
Joe White

1
감사합니다 켄트. 이 "SizeObserver"클래스에 대한 코드 샘플을 아래에 게시했습니다.
Scott Whitlock

52
이 감정에 +1 : "읽기 전용 DP는 OneWayToSource 바인딩을 지원해야합니다."
Tristan

3
SizeHeigth와 Width를 결합하여 하나의 속성 만 만드는 것이 더 좋습니다 . 대략. 50 % 적은 코드.
Gerard

1
@Gerard : .NET에는 ActualSize속성 이 없기 때문에 작동하지 않습니다 FrameworkElement. 당신이 연결된 속성의 결합을 직접하려는 경우, 당신은 두 가지 속성에 바인딩 될 작성해야 ActualWidth하고 ActualHeight각각을.
dotNET

59

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

(사용자 543564의 답변을 통해) : 이것은 답변이 아니라 Dmitry에 대한 의견입니다. 귀하의 솔루션을 사용했으며 훌륭하게 작동했습니다. 다른 장소에서 일반적으로 사용할 수있는 멋진 범용 솔루션입니다. 일부 UI 요소 속성 (ActualHeight 및 ActualWidth)을 뷰 모델에 푸시하는 데 사용했습니다.
Marc Gravell

2
감사! 이것은 내가 정상적인 재산을 얻는 데 도움이되었습니다. 불행히도 속성은 INotifyPropertyChanged 이벤트를 게시하지 않았습니다. DataPipe 바인딩에 이름을 할당하고 컨트롤 변경 이벤트에 다음을 추가하여이 문제를 해결했습니다. BindingOperations.GetBindingExpressionBase (bindingName, DataPipe.SourceProperty) .UpdateTarget ();
chilltemp 2011 년

3
이 솔루션은 저에게 잘 맞았습니다. 내 유일한 조정은 TargetProperty DependencyProperty의 FrameworkPropertyMetadata에 대해 BindsTwoWayByDefault를 true로 설정하는 것입니다.
Hasani Blackwell 2011

1
이 솔루션에 대한 유일한 불만은 Target외부에서 변경해서는 안되지만 속성을 쓰기 가능하게 만들어야 하므로 깨끗한 캡슐화를 깨는 것 같습니다 .-/
또는 Mapper

코드를 복사하여 붙여 넣는 것보다 NuGet 패키지를 선호하는 사람들을 위해 : 내 오픈 소스 JungleControls 라이브러리에 DataPipe를 추가했습니다. DataPipe 문서를 참조하십시오 .
Robert Važan 2014

21

다른 사람이 관심이 있다면 여기에 Kent 솔루션의 근사치를 코딩했습니다.

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
}

앱에서 자유롭게 사용하십시오. 잘 작동한다. (Kent에게 감사합니다!)


10

여기에 내가 블로그에 올린이 "버그"에 대한 또 다른 솔루션이 있습니다.
OneWayToSource Binding for ReadOnly Dependency Property

두 개의 종속성 속성 인 Listener와 Mirror를 사용하여 작동합니다. 리스너는 OneWay를 TargetProperty에 바인딩하고 PropertyChangedCallback에서 OneWayToSource에 바인딩 된 Mirror 속성을 Binding에 지정된대로 업데이트합니다. 나는 그것을 호출하고 다음 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 바인딩이 업데이트 한 후 소스에서 값을 다시 읽어 오기 때문에이를위한 기본 제공 지원에서 훨씬 더 멀리 떨어져 있습니다.


Stack Overflow에 대한 답변은 완전히 독립적이어야합니다. 선택적 외부 참조에 대한 링크를 포함하는 것은 좋지만 답변에 필요한 모든 코드는 답변 자체에 포함되어야합니다. 다른 웹 사이트를 방문하지 않고도 사용할 수 있도록 질문을 업데이트하십시오.
Peter Duniho

4

나는 Dmitry Tashkinov의 솔루션을 좋아합니다! 그러나 그것은 디자인 모드에서 내 VS를 추락했습니다. 그래서 OnSourceChanged 메서드에 줄을 추가했습니다.

    개인 정적 무효 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
조금 더 간단 할 수 있지만 잘 읽으면 요소에 이러한 바인딩을 하나만 허용 합니다 . 내 말은,이 접근법을 사용하면 ActualWidth ActualHeight를 둘 다 바인딩 할 수 없을 것이라고 생각합니다 . 그들 중 하나입니다.
quetzalcoatl
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.