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