Натискання властивостей графічного інтерфейсу лише для читання назад у ViewModel


124

Я хочу написати ViewModel, який завжди знає поточний стан деяких властивостей залежності лише для читання з View.

Зокрема, мій графічний інтерфейс містить FlowDocumentPageViewer, який відображає по одній сторінці одночасно з FlowDocument. FlowDocumentPageViewer відкриває два властивості залежності лише для читання під назвою CanGoToPreviousPage та CanGoToNextPage. Я хочу, щоб мій ViewModel завжди знав значення цих двох властивостей View.

Я зрозумів, що я можу це зробити за допомогою прив'язки даних OneWayToSource:

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

Якби це було дозволено, було б ідеально: щоразу, коли зміна властивості FlowGoToNextPage 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властивості. Отже, то Widthі Heightв моделі уявлення завжди синхронізований з ActualWidthі ActualHeightз UserControl.

Можливо, не ідеальне рішення (я згоден - DP-версії лише для читання повинні підтримувати OneWayToSourceприв’язки), але це працює і підтримує схему MVVM. Очевидно, що ObservedWidthі ObservedHeightDP не є лише для читання.

ОНОВЛЕННЯ: ось код, який реалізує функціонал, описаний вище:

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 до цього настрою: "ДП лише для читання повинні підтримувати прив’язки OneWayToSource"
Трістан,

3
Можливо, навіть краще створити лише одну Sizeвластивість, поєднуючи Heigth і Width. Прибл. На 50% менше коду.
Джерард

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): Це не відповідь, а коментар Дмитру - я використав ваше рішення, і воно спрацювало чудово. Приємне універсальне рішення, яке можна використовувати в різних місцях. Я використовував його для введення деяких властивостей елементів інтерфейсу (ActualHeight та ActualWidth) у свій мод перегляду.
Марк Гравелл

2
Дякую! Це допомогло мені прив'язатись до нормального отримання лише власності. На жаль, власність не публікувала події INotifyPropertyChanged. Я вирішив це, призначивши ім’я для прив’язки DataPipe і додавши наступне до події, зміненої в елементах управління: BindingOperations.GetBindingExpressionBase (indingName, DataPipe.SourceProperty) .UpdateTarget ();
chilltemp

3
Це рішення добре працювало для мене. Єдиним моїм налаштуванням було встановлення BindsTwoWayByDefault для істинного для FrameworkPropertyMetadata на TargetProperty DependencyProperty.
Хасані Блеквелл

1
Єдиною перевагою цього рішення, здається, є те, що воно порушує чисту інкапсуляцію, оскільки Targetвластивість має бути доступною для запису, навіть якщо її не слід змінювати ззовні: - /
АБО Mapper

Для тих, хто вважає за краще пакет NuGet над копіюванням n-вставлення коду: я додав DataPipe до своєї бібліотеки 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 для властивості залежності ReadOnly

Він працює за допомогою двох властивостей залежності, слухача та дзеркала. Слухач прив’язаний OneWay до властивості TargetProperty, а в PropertyChangedCallback оновить властивість Mirror, яка пов'язана OneWayToSource до того, що було визначено в Прив'язці. Я називаю це, PushBindingі його можна встановити на будь-якому властивості Dependpendation, доступному лише для читання

<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 ми ще більше віддалені від вбудованої підтримки для цього, оскільки a прив'язка OneWayToSource зчитує значення з джерела після оновлення його


Відповіді на переповнення стека повинні бути повністю самостійними. Добре включати посилання на необов'язкові зовнішні посилання, але весь код, необхідний для відповіді, повинен бути включений у саму відповідь. Будь ласка, оновіть своє запитання, щоб ним можна було користуватися, не відвідуючи жодного іншого веб-сайту.
Петро Дуніхо

4

Мені подобається рішення Дмитра Ташкінова! Однак він розбив мій 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.