ReactiveUI - 为什么这些 {Binding } 有效?

ReactiveUI - why do these {Binding }s work?

我在 codereview (https://codereview.stackexchange.com/questions/124300/reactiveui-and-wpf-reusing-a-value-to-update-multiple-properties) 上发布了一个问题。我最近在回答这个问题时所做的努力使我得到了以下代码,它似乎可以工作,但我不确定实际提供功能的机制是什么!

MainWindow.xaml

<Window x:Class="TestHumanName.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="392" Width="391">
    <StackPanel Orientation="Vertical">
        <StackPanel Orientation="Horizontal">
            <Label Content="Full" />
            <TextBox Width="100" Text="{Binding Full, UpdateSourceTrigger=PropertyChanged}"/>
            <Button Content="Go"/>
        </StackPanel>
        <StackPanel Orientation="Horizontal">
            <Label Content="Title" />
            <TextBox Width="100" Text="{Binding NameObject.Title, Mode=OneWay}"/>
        </StackPanel>
        <StackPanel Orientation="Horizontal">
            <Label Content="First" />
        <TextBox Width="100" Text="{Binding NameObject.First, Mode=OneWay}"/>
        </StackPanel>
        <StackPanel Orientation="Horizontal">
            <Label Content="Middle" />
        <TextBox Width="100" Text="{Binding NameObject.Middle, Mode=OneWay}"/>
        </StackPanel>
        <StackPanel Orientation="Horizontal">
            <Label Content="Last" />
        <TextBox Width="100" Text="{Binding NameObject.Last, Mode=OneWay}"/>
        </StackPanel>
    </StackPanel>
</Window>

MainWindow.xaml.cs:

using System.Windows;
using TestHumanName.ViewModel;

namespace TestHumanName
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = new MainViewModel();
        }
    }
}

MainViewModel.cs

public class MainViewModel : ReactiveObject
{
    public MainViewModel()
    {
        this.WhenAnyValue(x => x.Full).Where(x => x != null).Select(x => ParseName(x))
            .ToProperty(this, x => x.NameObject, out __oapName);
    }

    private string __sFull;
    public string Full
    {
        get { return __sFull; }
        set { this.RaiseAndSetIfChanged(ref __sFull, value); }
    }

    readonly ObservableAsPropertyHelper<Name> __oapName;
    public Name NameObject { get { return __oapName.Value; } }

    //NAME PARSING CODE BELOW THIS LINE

        public class Name
        {
            public string Title { get; set; }
            public string First { get; set; }
            public string Middle { get; set; }
            public string Last { get; set; }
            public string Suffix { get; set; }
        }

        public Name ParseName(string s)
        {
            Name n = new Name();

            // Split on period, commas or spaces, but don't remove from results.
            List<string> parts = Regex.Split(s, @"(?<=[., ])").ToList();

            // Remove any empty parts
            for (int x = parts.Count - 1; x >= 0; x--)
                if (parts[x].Trim() == "")
                    parts.RemoveAt(x);

            if (parts.Count > 0)
            {
                // Might want to add more to this list
                string[] prefixes = { "mr", "mrs", "ms", "dr", "miss", "sir", "madam", "mayor", "president" };

                // If first part is a prefix, set prefix and remove part
                string normalizedPart = parts.First().Replace(".", "").Replace(",", "").Trim().ToLower();
                if (prefixes.Contains(normalizedPart))
                {
                    n.Title = parts[0].Trim();
                    parts.RemoveAt(0);
                }
            }

            if (parts.Count > 0)
            {
                // Might want to add more to this list, or use code/regex for roman-numeral detection
                string[] suffixes = { "jr", "sr", "i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix", "x", "xi", "xii", "xiii", "xiv", "xv" };

                // If last part is a suffix, set suffix and remove part
                string normalizedPart = parts.Last().Replace(".", "").Replace(",", "").Trim().ToLower();
                if (suffixes.Contains(normalizedPart))
                {
                    n.Suffix = parts.Last().Replace(",", "").Trim();
                    parts.RemoveAt(parts.Count - 1);
                }
            }

            // Done, if no more parts
            if (parts.Count == 0)
                return n;

            // If only one part left...
            if (parts.Count == 1)
            {
                // If no prefix, assume first name, otherwise last
                // i.e.- "Dr Jones", "Ms Jones" -- likely to be last
                if (n.Title == "")
                    n.First = parts.First().Replace(",", "").Trim();
                else
                    n.Last = parts.First().Replace(",", "").Trim();
            }

            // If first part ends with a comma, assume format:
            //   Last, First [...First...]
            else if (parts.First().EndsWith(","))
            {
                n.Last = parts.First().Replace(",", "").Trim();
                for (int x = 1; x < parts.Count; x++)
                    n.First += parts[x].Replace(",", "").Trim() + " ";
                n.First = n.First.Trim();
            }

            // Otherwise assume format:
            // First [...Middle...] Last

            else
            {
                n.First = parts.First().Replace(",", "").Trim();
                n.Last = parts.Last().Replace(",", "").Trim();
                for (int x = 1; x < parts.Count - 1; x++)
                    n.Middle += parts[x].Replace(",", "").Trim() + " ";
                if (n.Middle != null) n.Middle = n.Middle.Trim();
            }

            return n;
        }
    }
}

我不明白的是,我如何替换 NameObject 属性 的值,而 {Binding ...}s 神奇地知道它们应该更新。当然,替换 NameObject 属性 的内容不会在其子属性上调用 OnPropertyChanged ... Name class 甚至没有实现 INotifyPropertyChanged。

所以,这是怎么回事?

谢谢。

一般概念

我想你的误解从何而来。 你在这里做什么:

this.WhenAnyValue(x => x.Full).Where(x => x != null).Select(x => ParseName(x))
        .ToProperty(this, x => x.NameObject, out __oapName);

意味着每当 Full 属性 改变时,触发 ParseName 方法,这样它就会更新 属性 _oapName。 但是,由于 _oapNameObservableAsPropertyHelper 类型,这是 Rx 的核心,它会自己通知 UI。

如文档所述:

This initialises the 'put yours here' property (an ObservableAsPropertyHelper property) as a property that will be updated with the current search text length every time it changes. The property cannot be set in any other manner and raises change notifications, so can itself be used in a WhenAny expression or a binding.

来源: http://docs.reactiveui.net/en/user-guide/when-any/index.html

幕后花絮

如果我们看一下 ToProperty 的签名:

public static ObservableAsPropertyHelper<TRet> ToProperty<TObj, TRet>(
    this IObservable<TRet> This,
    TObj source,
    Expression<Func<TObj, TRet>> property,
    out ObservableAsPropertyHelper<TRet> result,
    TRet initialValue = default(TRet),
    IScheduler scheduler = null)
    where TObj : IReactiveObject
    {
        var ret = source.observableToProperty(This, property, initialValue, scheduler);

        result = ret;
        return ret;
    }

我们将看到它实际上做了什么,只是调用扩展方法 observableToProperty 来生成 ObservableAsPropertyHelper。 更准确地说,在 observableToProperty 中,我们可以看到像这样的几行:

var ret = new ObservableAsPropertyHelper<TRet>(observable,
            _ => This.raisePropertyChanged(name),
            _ => This.raisePropertyChanging(name),
            initialValue, scheduler);

并牢记:

  • This(变量名称的荣誉)在 observableToProperty 中属于 TObj 类型,具有 where TObj : IReactiveObject
  • 的约束
  • name 是您传递给 Expression<Func<TObj, TRet>> property 的 属性 的名称,即。在您的情况下,来自 x => x.NameObjectname 将是 NameObject

它最终会在 NameObject 的父级 ReactiveObject 上引发 RaisePropertyChanged,即 MainViewModel

源代码:ObservableAsPropertyHelper.cs @ ReactiveUI on Github