WPF Уведомлять об изменении содержимого при изменении его свойства - PullRequest
0 голосов
/ 04 января 2019

Я установил метку Content для некоторого пользовательского класса:

<Label>
    <local:SomeContent x:Name="SomeContent" some="abc" />
</Label>

Это правильно отображает «abc» в представлении. Однако я не могу понять, как уведомить Label, что свойство содержимого изменилось, то есть это:

SomeContent.some = "xyz";

Не заставляет ярлык обновлять свой вид.

Я знаю, что могу установить привязку к свойству Content метки. Мне уже нравится 7 различных рабочих методов для достижения автоматического обновления Тем не менее, я заинтересован в этом конкретном поведении, потому что это сэкономит мне массу работы в некоторых сценариях, т.е. требования:

  • Содержимое метки всегда совпадает с экземпляром SomeContent, изменяются только его свойства.
  • Нет привязки содержимого ярлыка. Метка должна принимать объект содержимого и обновляться всякий раз, когда содержимое изменяется.
  • Начальное значение свойства some может быть установлено в XAML
  • some свойство может быть изменено в коде, вызывая обновление метки.

Я что-то упустил, или это невозможно?

Это моя текущая реализация SomeContent:

public class SomeContent : DependencyObject, INotifyPropertyChanged {

    public static readonly DependencyProperty someProperty =
        DependencyProperty.Register(nameof(some), typeof(string),
            typeof(SomeContent),
            new PropertyMetadata("", onDPChange)
    );

    private static void onDPChange(DependencyObject d, DependencyPropertyChangedEventArgs e) {
        //throw new NotImplementedException();
        (d as SomeContent).some = e.NewValue as String;
    }

    public event PropertyChangedEventHandler PropertyChanged;

    public string some {
        get => (string)GetValue(someProperty);
        set {
            SetValue(someProperty, value);
            PropertyChanged?.Invoke(this,
                new PropertyChangedEventArgs(nameof(some))
            );
        }
    }


    public override string ToString() => some;
}

Ответы [ 2 ]

0 голосов
/ 08 января 2019

Оказывается, это невозможно сделать без кода третьей стороны. Поэтому я написал вспомогательный класс, чтобы сделать это легко.

Динамический объект

public class SomeContent : IChangeNotifer {
    public event Action<object> MODIFIED;

    private string _some;
    public string some {
        get => _some;
        set {
            _some = value;
            MODIFIED?.Invoke(this);
        }
    }

    public override string ToString() => some;
}

Вы можете добавить его в файл xmal , и он будет обновлен автоматически. Единственным дополнительным шагом является добавление UIReseter где-то ниже элементов, которые должны быть автоматически обновлены, но требуется только один для нескольких элементов в дереве.

Использование

<Window x:Class="DependencyContentTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:DependencyContentTest"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel>
        <local:UIReseter />
        <Label>
            <local:SomeContent x:Name="SomeContent" some="abcd" />
        </Label>
        <Grid>
            <Label>
                <local:SomeContent x:Name="nested" some="nyest"/>
            </Label>
        </Grid>
    </StackPanel>
</Window>

Код главного окна

public partial class MainWindow : Window {

    private Timer t;

    public MainWindow() {
        InitializeComponent();
        t = new Timer(onTimer, null, 5000, Timeout.Infinite);
        MouseDown += (s,e) => { SomeContent.some = "iii"; };
    }

    private void onTimer(object state) {
        Dispatcher.Invoke(() => {
            SomeContent.some = "aaaa";
            nested.some = "xxx";
        });
    }
}

А это вспомогательный класс , который обрабатывает обновление

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using H = System.Windows.LogicalTreeHelper;
using FE = System.Windows.FrameworkElement;
using DO = System.Windows.DependencyObject;
using System.Reflection;
using System.Windows.Markup;

namespace DependencyContentTest
{
    public interface IChangeNotifer {
        /// <summary>Dispatched when this object was modified.</summary>
        event Action<object> MODIFIED;
    }

    /// <summary>This element tracks nested <see cref="IChangeNotifer"/> descendant objects (in logical tree) of this object's parent element and resets a child in it's panel property.
    /// Only static (XAML) objects are supported i.e. object added to the tree dynamically at runtime will not be tracked.</summary>
    public class UIReseter : UIElement {

        public int searchDepth { get; set; } = int.MaxValue;

        protected override void OnVisualParentChanged(DO oldParent){
            if (VisualParent is FE p) p.Loaded += (s, e)  =>  bind(p);
        }

        private void bind(FE parent, int dl = 0) {
            if (parent == null || dl > searchDepth) return;
            var chs = H.GetChildren(parent);
            foreach (object ch in chs) {
                if (ch is UIReseter r && r != this) throw new Exception($@"There's overlapping ""{nameof(UIReseter)}"" instance in the tree. Use single global instance of check ""{nameof(UIReseter.searchDepth)}"" levels.");
                if (ch is IChangeNotifer sc) trackObject(sc, parent);
                else bind(ch as FE, ++dl);
            }
        }

        private Dictionary<IChangeNotifer, Reseter> tracked = new Dictionary<IChangeNotifer, Reseter>();
        private void trackObject(IChangeNotifer sc, FE parent) {
            var cp = getContentProperty(parent);
            if (cp == null) return;
            var r = tracked.nev(sc, () => new Reseter {
                child = sc,
                parent = parent,
                content = cp,
            });
            r.track();
        }

        private PropertyInfo getContentProperty(FE parent) {
            var pt = parent.GetType();
            var cp = parent.GetType().GetProperties(
                BindingFlags.Public |
                BindingFlags.Instance
            ).FirstOrDefault(i => Attribute.IsDefined(i,
                typeof(ContentPropertyAttribute)));
            return cp ?? pt.GetProperty("Content");
        }

        private class Reseter {
            public DO parent;
            public IChangeNotifer child;
            public PropertyInfo content;
            private bool isTracking = false;

            /// <summary>Function called by <see cref="IChangeNotifer"/> on <see cref="IChangeNotifer.MODIFIED"/> event.</summary>
            /// <param name="ch"></param>
            public void reset(object ch) {
                if(! isChildOf(child, parent)) return;
                //TODO: Handle multi-child parents
                content.SetValue(parent, null);
                content.SetValue(parent, child);
            }

            public void track() {
                if (isTracking) return;
                child.MODIFIED += reset;
            }

            private bool isChildOf(IChangeNotifer ch, DO p) {
                if(ch is DO dch) {
                    if (H.GetParent(dch) == p) return true;
                    child.MODIFIED -= reset; isTracking = false;
                    return false;
                }
                var chs = H.GetChildren(p);
                foreach (var c in chs) if (c == ch) return true;
                child.MODIFIED -= reset; isTracking = false;
                return false;
            }
        }
    }

    public static class DictionaryExtension {
        public static V nev<K,V>(this Dictionary<K,V> d, K k, Func<V> c) {
            if (d.ContainsKey(k)) return d[k];
            var v = c(); d.Add(k, v); return v;
        }
    }

}

Может быть улучшено и не полностью протестировано, но работает в текущих целях. Дополнительная проблема заключается в том, что некоторые элементы, такие как TextBox, кричат ​​о том, что не поддерживают SomeContent, как будто его так трудно использовать ToString() ... но это другая история, и она не связана с моим вопросом.

0 голосов
/ 05 января 2019

Обновленный ответ:

Я бы отбросил реализацию SomeContent в качестве свойства Dependency и вместо этого использовал бы UserControl:

<UserControl x:Class="WpfApp1.SomeContent"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:WpfApp1"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>
            <TextBlock Text="{Binding some, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:SomeContent}}}"/>
    </Grid>
</UserControl>

Затем в коде позади:

/// <summary>
/// Interaction logic for SomeContent.xaml
/// </summary>
public partial class SomeContent : UserControl
{
    public static readonly DependencyProperty someProperty =
        DependencyProperty.Register(nameof(some), typeof(string),
            typeof(SomeContent),
            new PropertyMetadata("")
        );

    public string some
    {
        get => (string)GetValue(someProperty);
        set => SetValue(someProperty, value);
    }

    public SomeContent()
    {
        InitializeComponent();
    }
}

Далее, реализуйте модель представления, которая реализует INotifyPropertyChanged:

public class MyViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private string _somePropertyOnMyViewModel;
    public string SomePropertyOnMyViewModel
    {
        get => _somePropertyOnMyViewModel;
        set { _somePropertyOnMyViewModel = value; OnPropertyChanged(); }
    }
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

Затем создайте экземпляр MyViewModel в своем представлении и назначьте его для DataContext вашего представления:

public class MyView : Window
{
    public MyView()
    {
        InitializeComponent();
        DataContext = new MyViewModel();
    }
}

Затем, наконец, в MyView используйте разметку, предоставленную в моем исходном ответе:

<Label>
    <local:SomeContent x:Name="SomeContent" some="{Binding 
SomePropertyOnMyViewModel" />
</Label>
...