Xamarin IMarkupExtension,获取 ViewModel 属性 值

Xamarin IMarkupExtension, Get ViewModel Property Values

下面是 MvxLang 的修改实现。

我的目标是能够使用存储在 .json 文件中的现有字符串值简洁地实现屏幕 reader 文本,以及从中检索到的动态生成的文本.cs 文件。

我希望使用以下语法:

xct:SemanticEffect.Description="{mvx:MvxLang ViewModel.SomeStringFromCsFile | SomeStringFromJsonFile | ViewModel.SomeOtherStringFromCsFile}"

这样我们的 ViewModels/Xaml 就不会被屏幕 reader 文本 logic/markup.

臃肿

当仅从 .json 文件中检索字符串值时,我的实现工作正常,但我也希望使用 .cs 文件中的各种值...

调用 GetValue() 时,我的问题出现在这段代码中,我可以 return 值,但似乎在 ViewModel:

之前调用了 IMarkupExtensions
var prefix = "ViewModel.";
if (str.Contains(prefix))
{
    var vm = (rootObject is MvxContentPage)
        ? ((MvxContentPage)rootObject).GetViewModel()
        : ((MvxContentView)rootObject).GetViewModel();
    PropertyInfo prop = vm.GetType().GetProperty(str.Replace(prefix, string.Empty));
    var propValue = prop.GetValue(vm);
    return propValue as string ?? string.Empty;
}

有没有办法 return 这里的运行时值?

这是代码的其余部分:

[ContentProperty("Source")]
public class MvxLang : IMarkupExtension
{
    readonly static IMvxTextProvider _textProvider = Mvx.IoCProvider.Resolve<IMvxTextProvider>();
    public static string TransitioningViewModel { private get; set; }
    public string Source { set; get; }

    public object ProvideValue(IServiceProvider serviceProvider)
    {
        var valueProvider = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
        var rootProvider = serviceProvider.GetService(typeof(IRootObjectProvider)) as IRootObjectProvider;

        object rootObject = null;
        if (rootProvider == null)
        {
            var propertyInfo = valueProvider.GetType()
                                            .GetTypeInfo()
                                            .DeclaredProperties
                                            .FirstOrDefault(dp => dp.Name.Contains("ParentObjects"));

            var parentObjects = (propertyInfo.GetValue(valueProvider) as IEnumerable<object>).ToList();
            rootObject = parentObjects.Last();
        }
        else
            rootObject = rootProvider.RootObject;

        var name = string.Empty;
        if (!(rootObject is MvxContentPage || rootObject is MvxContentView))
        {
            // Transitioning
            name = TransitioningViewModel;
        }
        else
        {
            var page = (VisualElement)rootObject;
            name = page.GetType().BaseType.GetGenericArguments()[0].Name;    
        }

        if (!string.IsNullOrEmpty(name))
        {
            var value = string.Empty;
            (bool, string) targetPropertyCheck = this.TargetPropertyCheck_ADA(valueProvider.TargetProperty);

            if (targetPropertyCheck.Item1)
            {
                value = ProvideValue_ADA(targetPropertyCheck.Item2, _textProvider, rootObject, name, Source);
                return value;
            }
            else
            {
                value = _textProvider.GetText(name, Source);
                return value;
            }
        }

        return string.Empty;
    }

    public (bool, string) TargetPropertyCheck_ADA(object targetProperty)
    {
        var propertyName = string.Empty;
        var isADA = false;
        if (targetProperty is BindableProperty _targetProperty)
        {
            if (_targetProperty.DeclaringType.Name.Equals("SemanticEffect"))
            {
                propertyName = _targetProperty.PropertyName;
                isADA = propertyName.Equals("Description") || propertyName.Equals("Hint");
            }
        }
        return (isADA, propertyName);
    }

    public string ProvideValue_ADA( string propertyName, IMvxTextProvider textProvider, object rootObject, string name, string keyString)
    {
        if (!string.IsNullOrEmpty(keyString) && !string.IsNullOrEmpty(propertyName))
        {
            switch (propertyName)
            {
                case "Description":
                    if (keyString.Contains('|'))
                    {
                        var parameters = keyString.Split(new char[] { '|' });
                        IEnumerable<string> appliedStrings = parameters.Select(s =>
                        {
                            var str = s.Trim();
                            var prefix = "ViewModel.";
                            if (str.Contains(prefix))
                            {
                                var vm = (rootObject is MvxContentPage)
                                    ? ((MvxContentPage)rootObject).GetViewModel()
                                    : ((MvxContentView)rootObject).GetViewModel();
                                PropertyInfo prop = vm.GetType().GetProperty(str.Replace(prefix, string.Empty));
                                var propValue = prop.GetValue(vm);
                                return propValue as string ?? string.Empty;
                            }
                            else
                            {
                                return textProvider.GetText(name, str);
                            }
                        });
                        return string.Join(", ", appliedStrings);
                    }
                    else
                    {
                        return textProvider.GetText(name, keyString);
                    }
                case "Hint":
                    var appliedText = textProvider.GetText(name, keyString);
                    return $"Double tap to {appliedText}";
                default:
                    break;
            }
        }

        return string.Empty;
    }
}

在意识到 IMarkupExtension class 在 ViewModel 设置其属性之前触发后,最终采用了此解决方案。

[ContentProperty(nameof(Values))]
public class Provider : IMarkupExtension<MultiBinding>
{
    readonly static IMvxTextProvider _textProvider = Mvx.IoCProvider.Resolve<IMvxTextProvider>();

    public string Values { set; get; }
    IList<BindingBase> Bindings { get; set; } = new List<BindingBase>();
    string StringFormat { get; set; } = "{0}";

    object IMarkupExtension.ProvideValue(IServiceProvider serviceProvider) => ProvideValue(serviceProvider);
    public MultiBinding ProvideValue(IServiceProvider serviceProvider)
    {
        var valueProvider = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
        var _property = valueProvider?.TargetProperty as BindableProperty;

        object rootObject = null;
        var propertyInfo = valueProvider.GetType()
                                        .GetTypeInfo()
                                        .DeclaredProperties
                                        .FirstOrDefault(dp => dp.Name.Contains("ParentObjects"));

        var parentObjects = (propertyInfo.GetValue(valueProvider) as IEnumerable<object>).ToList();
        rootObject = parentObjects.Last();

        var name = string.Empty;
        var page = (VisualElement)rootObject;
        name = page.GetType().BaseType.GetGenericArguments()[0].Name;

        if (!string.IsNullOrEmpty(name))
        {
            var propertyName = _property.PropertyName;

            if (SemanticPropertyCheck(_property, propertyName))
            {
                BuildBindings(propertyName, name, Values);
            }
            else
            {
                Bindings.Add(new Binding()
                {
                    Path = $"[{name}|{Values}]",
                    Source = InternalValue.Instance
                });
            }
        }

        return new MultiBinding()
        {
            Bindings = Bindings,
            StringFormat = StringFormat
        };
    }

    bool SemanticPropertyCheck(BindableProperty property, string name) =>
        property.DeclaringType.Name.Equals("SemanticEffect") && (name.Equals("Description") || name.Equals("Hint"));

    void BuildBindings(string propertyName, string name, string valueString)
    {
        if (!string.IsNullOrEmpty(valueString) && !string.IsNullOrEmpty(propertyName))
        {
            switch (propertyName)
            {
                case "Description":
                    if (valueString.Contains('|'))
                    {
                        var values = (valueString.Split(new char[] { '|' }) as IEnumerable<string>).ToList();
                        
                        values.ForEach(s =>
                        {
                            var index = values.IndexOf(s);
                            Bindings.Add(CreateBinding(name, s.Trim(), false));
                            if (index > 0)
                                StringFormat += $", {{{index}}}";
                        });
                    }
                    else
                    {
                        Bindings.Add(CreateBinding(name, valueString, false));
                    }
                    break;
                case "Hint":
                    Bindings.Add(CreateBinding(name, valueString, true));
                    break;
                default:
                    break;
            }
        }
    }

    BindingBase CreateBinding(string name, string key, bool isHint)
    {
        var prefix = "ViewModel.";
        return (key.Contains(prefix))
            ? new Binding() { Path = key.TrimStart(prefix.ToCharArray()) }
            : new Binding() { Path = $"[{name}|{key}]", Source = InternalValue.Instance };
    }

    sealed class InternalValue
    {
        readonly static IMvxTextProvider _textProvider = Provider._textProvider;
        public static InternalValue Instance { get; } = new InternalValue();
        public static InternalValue HintInstance { get; } = new InternalValue() { _isHint = true };

        bool _isHint { get; set; } = false;

        public string this[string _nameKey] => GetText(_nameKey.Split('|'));

        private string GetText(string[] nameKey)
        {
            var name = nameKey[0];
            var key = nameKey[1];
            var prefix = _isHint
                ? "Double tap to "
                : string.Empty;
            var appliedText = _textProvider.GetText(name, key);
            return $"{prefix}{appliedText}";
        }
    }
}