Home > Software design >  Xamarin IMarkupExtension, Get ViewModel Property Values
Xamarin IMarkupExtension, Get ViewModel Property Values

Time:12-19

Below is a modified implementation of MvxLang.

My goal is to be able to implement screen reader text concisely with existing string values stored in <ViewModelName>.json files in our projects Resources, as well as dynamically generated text retrieved from <ViewModelName>.cs files.

I wish to use the following syntax:

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

This way our ViewModels/Xaml will not be bloated with screen reader text logic/markup.

My implementation works fine when only retrieving string value from <ViewModelName>.json files, but I wish to use a variety of values from <ViewModelName>.cs files as well...

My troubles occur in this block of code when calling GetValue(), I can return the value, but it appears IMarkupExtensions are called before the the ViewModel:

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;
}

Is there a way to return the runtime values here?

Here is the rest of the code:

[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;
    }
}

CodePudding user response:

Ultimately landed on this solution after realizing that the IMarkupExtension class is triggered BEFORE the ViewModel has set its properties.

[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}";
        }
    }
}
  • Related