การพุชคุณสมบัติ GUI แบบอ่านอย่างเดียวกลับเข้าสู่ ViewModel


124

ฉันต้องการเขียน ViewModel ที่มักจะรู้สถานะปัจจุบันของคุณสมบัติการอ้างอิงแบบอ่านอย่างเดียวจาก View

โดยเฉพาะ GUI ของฉันมี FlowDocumentPageViewer ซึ่งแสดงทีละหน้าจาก FlowDocument FlowDocumentPageViewer แสดงคุณสมบัติการอ้างอิงแบบอ่านอย่างเดียวสองรายการที่เรียกว่า CanGoToPreviousPage และ CanGoToNextPage ฉันต้องการให้ ViewModel ของฉันทราบค่าของคุณสมบัติ View ทั้งสองนี้เสมอ

ฉันคิดว่าฉันสามารถทำได้ด้วยฐานข้อมูล OneWayToSource:

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

หากอนุญาตสิ่งนี้จะสมบูรณ์แบบ: เมื่อใดก็ตามที่คุณสมบัติ CanGoToNextPage ของ FlowDocumentPageViewer เปลี่ยนไปค่าใหม่จะถูกผลักลงไปในคุณสมบัติ NextPageAvailable ของ ViewModel ซึ่งเป็นสิ่งที่ฉันต้องการ

ขออภัยสิ่งนี้ไม่ได้รวบรวม: ฉันได้รับข้อผิดพลาดว่าคุณสมบัติ 'CanGoToPreviousPage' เป็นแบบอ่านอย่างเดียวและไม่สามารถตั้งค่าจากมาร์กอัปได้ เห็นได้ชัดว่าคุณสมบัติแบบอ่านอย่างเดียวไม่รองรับการเชื่อมต่อฐานข้อมูลใด ๆแม้แต่การเชื่อมต่อฐานข้อมูลที่เป็นแบบอ่านอย่างเดียวที่เกี่ยวกับคุณสมบัตินั้น

ฉันสามารถทำให้คุณสมบัติของ ViewModel ของฉันเป็น DependencyProperties และทำการผูก OneWay ไปในทางอื่น แต่ฉันไม่ได้คลั่งไคล้กับการละเมิดการแยกข้อกังวล (ViewModel ต้องการการอ้างอิงถึง View ซึ่งควรหลีกเลี่ยงการเชื่อมต่อฐานข้อมูล MVVM )

FlowDocumentPageViewer ไม่เปิดเผยเหตุการณ์ CanGoToNextPageChanged และฉันไม่รู้วิธีที่ดีในการรับการแจ้งเตือนการเปลี่ยนแปลงจาก DependencyProperty โดยไม่ต้องสร้าง DependencyProperty อื่นเพื่อผูกเข้ากับซึ่งดูเหมือนว่าจะใช้มากเกินไปที่นี่

ฉันจะแจ้งให้ ViewModel ของฉันทราบถึงการเปลี่ยนแปลงคุณสมบัติอ่านอย่างเดียวของมุมมองได้อย่างไร

คำตอบ:


152

ใช่ฉันเคยทำสิ่งนี้มาก่อนแล้วด้วยคุณสมบัติActualWidthและActualHeightซึ่งทั้งสองอย่างนี้เป็นแบบอ่านอย่างเดียว ฉันสร้างพฤติกรรมแนบที่มีObservedWidthและObservedHeightคุณสมบัติที่แนบมา นอกจากนี้ยังมีObserveคุณสมบัติที่ใช้ในการเริ่มต้นการเชื่อมต่อ การใช้งานมีลักษณะดังนี้:

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

ดังนั้นโมเดลมุมมองจึงมีWidthและHeightคุณสมบัติที่ซิงค์กับคุณสมบัติObservedWidthและที่ObservedHeightแนบมาเสมอ Observeคุณสมบัติเพียงแค่ยึดติดกับเหตุการณ์ของSizeChanged FrameworkElementในแฮนเดิลจะอัปเดตObservedWidthและObservedHeightคุณสมบัติ Ergo WidthและHeightของโมเดลมุมมองจะซิงค์กับActualWidthและActualHeightของUserControl.

อาจไม่ใช่วิธีแก้ปัญหาที่สมบูรณ์แบบ (ฉันยอมรับ - DP แบบอ่านอย่างเดียวควรรองรับการOneWayToSourceเชื่อมโยง) แต่ใช้งานได้และรักษารูปแบบ MVVM เห็นได้ชัดว่าDP 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
ฉันสงสัยว่าคุณสามารถใช้กลอุบายบางอย่างเพื่อแนบคุณสมบัติโดยอัตโนมัติโดยไม่จำเป็นต้องสังเกต แต่ดูเหมือนวิธีแก้ปัญหาที่ดี ขอบคุณ!
Joe White

1
ขอบคุณ Kent ฉันโพสต์ตัวอย่างโค้ดด้านล่างสำหรับคลาส "SizeObserver" นี้
Scott Whitlock

53
+1 สำหรับความเชื่อมั่นนี้: "DP แบบอ่านอย่างเดียวควรรองรับการเชื่อมโยง OneWayToSource"
Tristan

3
บางทีอาจจะดีกว่าที่จะสร้างSizeคุณสมบัติเพียงอย่างเดียวโดยรวมความสูงและความกว้างเข้าด้วยกัน ประมาณ รหัสน้อยลง 50%
Gerard

1
@Gerard: นั่นจะไม่ทำงานเพราะไม่มีคุณสมบัติใน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) ไปยัง viewmodel ของฉัน
Marc Gravell

2
ขอบคุณ! สิ่งนี้ช่วยให้ฉันผูกมัดกับการได้รับทรัพย์สินตามปกติเท่านั้น น่าเสียดายที่พร็อพเพอร์ตี้ไม่ได้เผยแพร่เหตุการณ์ INotifyPropertyChanged ฉันแก้ไขสิ่งนี้โดยกำหนดชื่อให้กับการเชื่อม DataPipe และเพิ่มสิ่งต่อไปนี้ในเหตุการณ์ที่เปลี่ยนแปลงการควบคุม: BindingOperations.GetBindingExpressionBase (bindingName, DataPipe.SourceProperty) .UpdateTarget ();
chilltemp

3
วิธีนี้ใช้ได้ผลดีสำหรับฉัน การปรับแต่งเพียงอย่างเดียวของฉันคือการตั้งค่า BindsTwoWayByDefault เป็น true สำหรับ FrameworkPropertyMetadata บน TargetProperty DependencyProperty
Hasani Blackwell

1
สิ่งเดียวที่จับได้เกี่ยวกับการแก้ปัญหานี้ดูเหมือนว่าจะทำให้การห่อหุ้มที่สะอาดแตกออกเนื่องจากTargetคุณสมบัติจะต้องทำให้สามารถเขียนได้แม้ว่าจะต้องไม่เปลี่ยนแปลงจากภายนอก: - /
หรือ Mapper

สำหรับผู้ที่ต้องการใช้แพ็คเกจ NuGet มากกว่าการคัดลอกและวางโค้ด: ฉันได้เพิ่ม DataPipe ลงในไลบรารี JungleControls ของโอเพนซอร์สแล้ว โปรดดูเอกสาร DataPipe
Robert Važan

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 สำหรับ ReadOnly Dependency Property

มันทำงานโดยใช้สองคุณสมบัติการพึ่งพาตัวฟังและกระจกเงา Listener ผูก OneWay กับ TargetProperty และใน PropertyChangedCallback จะอัพเดตคุณสมบัติ Mirror ซึ่งผูก OneWayToSource กับสิ่งที่ระบุไว้ใน 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 Binding จะอ่านค่ากลับจากแหล่งที่มาหลังจากอัปเดตแล้ว


คำตอบเกี่ยวกับ Stack Overflow ควรมีอยู่ในตัวเองทั้งหมด การรวมลิงก์ไปยังการอ้างอิงภายนอกที่เป็นทางเลือกนั้นทำได้ดี แต่ควรรวมรหัสทั้งหมดที่จำเป็นสำหรับคำตอบไว้ในคำตอบด้วย โปรดอัปเดตคำถามของคุณเพื่อให้สามารถใช้งานได้โดยไม่ต้องไปที่เว็บไซต์อื่น
Peter Duniho

4

ฉันชอบวิธีแก้ปัญหาของ Dmitry Tashkinov! อย่างไรก็ตามมันขัดข้อง VS ของฉันในโหมดออกแบบ นั่นเป็นเหตุผลที่ฉันเพิ่มบรรทัดในวิธี OnSourceChanged:

    โมฆะคงที่ส่วนตัว OnSourceChanged (DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ถ้า (! ((bool) DesignerProperties.IsInDesignModeProperty.GetMetadata (typeof (DependencyObject)). DefaultValue))
            ((DataPipe) ง) .OnSourceChanged (จ);
    }

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.