रीड-ओनली GUI गुणों को वापस ViewModel में धकेलना


124

मैं एक ViewModel लिखना चाहता हूं जो हमेशा व्यू से कुछ रीड-ओनली निर्भरता गुणों की वर्तमान स्थिति को जानता है।

विशेष रूप से, मेरे GUI में एक FlowDocumentPageViewer शामिल है, जो कि एक पृष्ठ को एक FlowDocument से एक समय में प्रदर्शित करता है। FlowDocumentPageViewer, CanGoToPrepretPage और CanGoToNextPage नामक दो रीड-ओनली निर्भरता गुणों को उजागर करता है। मैं चाहता हूं कि मेरा ViewModel हमेशा इन दो दृश्य गुणों के मूल्यों को जानता है।

मुझे लगा कि मैं OneWayToSource डेटाबाइंडिंग के साथ ऐसा कर सकता हूं:

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

यदि यह अनुमति दी गई थी, तो यह सही होगा: जब भी FlowDocumentPageViewer की CanGoToNextPage संपत्ति बदली जाती है, तो नया मान ViewModel के NextPageAvailable संपत्ति में नीचे धकेल दिया जाएगा, जो वास्तव में मुझे चाहिए।

दुर्भाग्य से, यह संकलन नहीं करता है: मुझे एक त्रुटि मिलती है जो कहती है कि 'CanGoToPrepretPage' संपत्ति केवल-पढ़ने के लिए है और इसे मार्कअप से सेट नहीं किया जा सकता है। जाहिरा तौर पर केवल-पढ़ने के गुण किसी भी प्रकार के डेटाबाइंडिंग का समर्थन नहीं करते हैं , यहां तक ​​कि उस संपत्ति के संबंध में केवल-पढ़ने योग्य डेटा भी नहीं है।

मैं अपने ViewModel के गुणों को DependencyProperties बना सकता हूं, और OneWay को दूसरे रास्ते पर जाने के लिए बाध्य कर सकता हूं, लेकिन मैं अलगाव-संबंधी चिंताओं के बारे में पागल नहीं हूं (ViewModel को व्यू के संदर्भ की आवश्यकता होगी, जो MVVM डेटाबाइंडिंग से बचने वाला है )।

FlowDocumentPageViewer एक CanGoToNextPageChanged ईवेंट को उजागर नहीं करता है, और मुझे डिपेंडेंसीप्रॉपर्टी से परिवर्तन सूचनाएं प्राप्त करने का कोई अच्छा तरीका नहीं पता है, इसे बांधने के लिए एक और डिपेंडेंसीप्रोपरेटी बनाने की कमी है, जो यहां ओवरकिल जैसा लगता है।

मैं अपने 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गुणों को अपडेट करता है । एर्गो, Widthऔर Heightदृश्य मॉडल हमेशा ActualWidthऔर ActualHeightके साथ सिंक में होता है UserControl

शायद सही समाधान नहीं है (मैं सहमत हूं - केवल पढ़ने के लिए डीपी कोOneWayToSource बाइंडिंग का समर्थन करना चाहिए ), लेकिन यह काम करता है और यह एमवीवीएस पैटर्न को बढ़ाता है। जाहिर है, 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
मुझे आश्चर्य है कि यदि आप बिना किसी ऑब्ज़र्वेशन के, गुणों को स्वचालित रूप से संलग्न करने के लिए कुछ प्रवंचना कर सकते हैं। लेकिन यह ठीक समाधान की तरह दिखता है। धन्यवाद!
जो व्हाइट

1
धन्यवाद केंट। मैंने इस "SizeObserver" वर्ग के लिए नीचे एक कोड नमूना पोस्ट किया।
स्कॉट व्हिटलॉक

52
+1 इस भावना के लिए: "केवल पढ़ने के लिए डीपी को वनवेस्ट सोर्स बाइंडिंग का समर्थन करना चाहिए"
ट्रिस्टन

3
शायद सिर्फ एक Sizeसंपत्ति बनाने के लिए बेहतर है , हीथ और चौड़ाई को मिलाकर। लगभग। 50% कम कोड।
गेरार्ड

1
@Gardard: क्योंकि वहाँ कोई ActualSizeसंपत्ति नहीं है काम नहीं करेगा FrameworkElement। यदि आप संलग्न गुणों का प्रत्यक्ष बंधन चाहते हैं, तो आपको क्रमशः ActualWidthऔर बाध्य होने के लिए दो गुण बनाने ActualHeightहोंगे।
डॉटनेट

59

मैं एक सार्वभौमिक समाधान का उपयोग करता हूं, जो न केवल एक्चुअलवेड और एक्चुअलहाइट के साथ काम करता है, बल्कि किसी भी डेटा के साथ आप कम से कम रीडिंग मोड में बांध सकते हैं।

मार्कअप इस तरह दिखता है, बशर्ते 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();
    }
}

(user543564 से एक जवाब के माध्यम से): यह एक जवाब नहीं है, लेकिन दिमित्री के लिए एक टिप्पणी - मैंने आपके समाधान का उपयोग किया और इसने बहुत अच्छा काम किया। अच्छा सार्वभौमिक समाधान जो विभिन्न स्थानों में उदारतापूर्वक उपयोग किया जा सकता है। मैंने इसका उपयोग अपने व्यूमॉडल में कुछ ui तत्व गुण (एक्चुअलहाइट और एक्चुअलविदथ) को धकेलने के लिए किया।
मार्क ग्रेवल

2
धन्यवाद! इससे मुझे केवल एक संपत्ति प्राप्त करने में मदद मिली। दुर्भाग्य से संपत्ति INotifyPropertyChanged घटनाओं को प्रकाशित नहीं किया। मैंने डेटापाइप बाइंडिंग में एक नाम निर्दिष्ट करके और नियंत्रण परिवर्तित घटना में निम्नलिखित को जोड़कर इसे हल किया: BindingOperations.GetBindingExpressionBase (बाइंडिंगनेम, डेटापाइप.सॉरप्रोपरेट्टी) .UpateateTarget ();
चिल्टपम्प

3
इस समाधान ने मेरे लिए अच्छा काम किया। मेरा एकमात्र ट्विंक टारप्रोपरेट्टी डिपेंडेंसीप्रोपरेटी पर FrameworkPropertyMetadata के लिए BindsTwoWayByDefault को सही पर सेट कर रहा था।
हसनी ब्लैकवेल

1
इस समाधान के बारे में एकमात्र पकड़ ऐसा प्रतीत होता है कि यह स्वच्छ अतिक्रमण को तोड़ता है, क्योंकि Targetसंपत्ति को लेखन योग्य बनाना पड़ता है, भले ही इसे बाहर से नहीं बदलना
पड़े

उन लोगों के लिए जो कोड कॉपी-एन-पेस्ट करने पर NuGet पैकेज पसंद करेंगे: मैंने अपने Openource JungleControls लाइब्रेरी में DataPipe जोड़ा है। डेटापाइप दस्तावेज़ देखें ।
राबर्ट वाजवान

21

अगर किसी और को दिलचस्पी है, तो मैंने यहां केंट के समाधान का अनुमान लगाया है:

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
}

अपने ऐप्स में इसका उपयोग करने के लिए स्वतंत्र महसूस करें। यह अच्छा काम करता है। (धन्यवाद केंट!)


10

यहाँ इस "बग" का एक और समाधान है, जिसके बारे में मैंने यहाँ ब्लॉग किया है:
OneWayToSource बाइंडिंग फॉरऑनलाइन प्रॉपर्टी

यह दो निर्भरता गुणों, श्रोता और दर्पण का उपयोग करके काम करता है। श्रोता OneWay को TargetProperty के लिए बाध्य करता है और PropertyChangedCallback में यह मिरर प्रॉपर्टी को अपडेट करता है, जो बाइंडिंग में निर्दिष्ट किए गए OneWayToSource से बाध्य है। मैं इसे कॉल करता हूं 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 बाइंडिंग ने इसे अपडेट करने के बाद सोर्स से वैल्यू को वापस पढ़ा।


स्टैक ओवरफ्लो पर उत्तर पूरी तरह से स्व-निहित होना चाहिए। वैकल्पिक बाहरी संदर्भों की लिंक शामिल करना ठीक है, लेकिन उत्तर के लिए आवश्यक सभी कोड को उत्तर में ही शामिल किया जाना चाहिए। कृपया अपने प्रश्न को अपडेट करें ताकि किसी अन्य वेब साइट पर जाकर इसका उपयोग किया जा सके।
पीटर डुनिहो

4

मुझे दिमित्री ताशकिनोव का समाधान पसंद है! हालाँकि इसने मेरे VS को डिज़ाइन मोड में क्रैश कर दिया। इसलिए मैंने OnSourceChanged पद्धति में एक पंक्ति जोड़ी:

    निजी स्थिर शून्य OnSourceChanged (निर्भरताओबजेक्ट d, DependencyPropertyChangedEventArgs e)
    {
        if (((bool) DesignerProperties.IsInDesignModeProperty.GetMetadata (typeof (DependencyObject))। DefaultValue)।
            ((DataPipe) घ) (ई) .OnSourceChanged;
    }

0

मुझे लगता है कि इसे थोड़ा सरल किया जा सकता है:

XAML:

behavior:ReadOnlyPropertyToModelBindingBehavior.ReadOnlyDependencyProperty="{Binding ActualWidth, RelativeSource={RelativeSource Self}}"
behavior:ReadOnlyPropertyToModelBindingBehavior.ModelProperty="{Binding MyViewModelProperty}"

सीएस:

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
थोड़ा सरल हो सकता है, लेकिन अगर मैं इसे अच्छी तरह से पढ़ता हूं, तो यह तत्व पर केवल एक ही बाध्यकारी है। मेरा मतलब है, मुझे लगता है कि इस दृष्टिकोण के साथ, आप वास्तविक और वास्तविक दोनों को बांधने में सक्षम नहीं होंगे । बस उनमें से एक है।
क्वेटज़ालकोट
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.