tmytのらくがき

個人の日記レベルです

XAMLにIsWindowsPhoneって書きたい、書きたいよね?

UniversalApp(not UWP)作ってるとXAMLをSharedプロジェクトに突っ込んで、x:Nameつけて、VisualStateとか#ifdefで電話だったらVisibilityを消して…とかやるよね、やるよね?でもそれXAMLで書きたいよね?ということでXAMLで書いてみた。

using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Windows.UI.Xaml;
// behavior sdk 必要
using Microsoft.Xaml.Interactions.Core;

namespace XamlConditions
{
    // あやしげなTypeConverter
    public class TypeConverter
    {
        public static object ChangeType(Type targetType, object from)
        {
            var typeInfo = targetType.GetTypeInfo();
            if (from == null)
                return typeInfo.IsValueType ? Activator.CreateInstance(targetType) : null;
            if (typeInfo.IsAssignableFrom(from.GetType().GetTypeInfo()))
                return from;
            // Convert
            var str = from.ToString();
            return typeInfo.IsEnum ? Enum.Parse(targetType, str) : ConvertType(str, targetType.FullName);
        }

        private static object ConvertType(string from, string type)
        {
            try
            {
                // Get Internal method
                var typeName = typeof(EventTriggerBehavior).AssemblyQualifiedName
                    .Replace(".EventTriggerBehavior,", ".TypeConverterHelper,");
                var typeConverterHelperType = Type.GetType(typeName);
                var typeConverterHelperConvert = typeConverterHelperType
                    .GetRuntimeMethod("Convert", new[] { typeof(string), typeof(string) });
                return typeConverterHelperConvert.Invoke(null, new object[] { from, type });
            }
            catch
            {
                return null;
            }
        }
    }

    // 本体
    public class IsWindowsPhone
    {
        #region Attached Property
        public static string GetOverride(DependencyObject obj)
        {
            return (string)obj.GetValue(OverrideProperty);
        }

        public static void SetOverride(DependencyObject obj, string value)
        {
            obj.SetValue(OverrideProperty, value);
        }

        public static readonly DependencyProperty OverrideProperty =
            DependencyProperty.RegisterAttached("Override", typeof(string), typeof(IsWindowsPhone), new PropertyMetadata(default(string), OnOverrideStringChanged));
        #endregion

        #region Previous Property Save Area
        private static Dictionary<string, object> GetPreviousPropertyBag(DependencyObject obj)
        {
            return (Dictionary<string, object>)obj.GetValue(PreviousPropertyBagProperty);
        }

        private static void SetPreviousPropertyBag(DependencyObject obj, Dictionary<string, object> value)
        {
            obj.SetValue(PreviousPropertyBagProperty, value);
        }

        public static readonly DependencyProperty PreviousPropertyBagProperty =
            DependencyProperty.RegisterAttached("PreviousPropertyBag", typeof(Dictionary<string, object>), typeof(IsWindowsPhone), new PropertyMetadata(null));
        #endregion

        private static void OnOverrideStringChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
#if WINDOWS_PHONE_APP
            UpdateProperty(d, e.NewValue as string);
#endif
        }

        private static void UpdateProperty(DependencyObject d, string e)
        {
            RestorePreviousProperties(d);
            if (string.IsNullOrWhiteSpace(e)) return;
            var props = ParseAttributesString(e);
            SaveCurrentProperties(d, props.Keys.AsEnumerable());
            // update values
            var type = d.GetType();
            foreach (var kv in props)
            {
                var prop = type.GetRuntimeProperty(kv.Key);
                var value = TypeConverter.ChangeType(prop.PropertyType, kv.Value);
                prop.SetValue(d, value);
            }
        }

        private static void SaveCurrentProperties(DependencyObject d, IEnumerable<string> names)
        {
            var type = d.GetType();
            var props = names.Select(s => type.GetRuntimeProperty(s));
            var bag = props.ToDictionary(p => p.Name, p => p.GetValue(d));
            SetPreviousPropertyBag(d, bag);
        }

        private static void RestorePreviousProperties(DependencyObject d)
        {
            var bag = GetPreviousPropertyBag(d);
            if (bag == null) return;
            var type = d.GetType();
            var props = bag.Keys.Select(s => type.GetRuntimeProperty(s));
            foreach (var p in props)
            {
                p.SetValue(d, bag[p.Name]);
            }
            SetPreviousPropertyBag(d, null);
        }

        private static Dictionary<string, string> ParseAttributesString(string attr)
        {
            var ret = new Dictionary<string, string>();
            string name = null;
            foreach (var v in attr.Split('='))
            {
                var n = v.LastIndexOf(",");
                if (n < 0)
                {
                    if (name != null) ret.Add(name.Trim(), v);
                    else name = v;
                    continue;
                }
                ret.Add(name.Trim(), v.Substring(0, n));
                name = v.Substring(n + 1);
            }
            return ret;
        }
    }
}

これを適当にこんな感じで

<Grid xmlns:c="using:XamlConditions">
    <!-- WindowsPhoneだけで見えるボタン -->
    <Button Visibility="Collapsed" c:IsWindowsPhone.Override="Visibility=Visible" />
</Grid>

すると電話だけででるボタンができます。がリフレクションしまくってるからあんまし早くないです。