TypeConverter

(xaml 에 string 을 값으로 넣기)




Color 는 문자열 "Red" 도 인식한다?


xaml 을 작성하다보면, 색과 관련된 Property 을 수시로 하게 됩니다. Rectangle 이나 Ellipse 의 Fill 을 하는 경우나, Window 배경색을 바꾼다거나 하는 것들 말이죠. 이 경우 xaml 에서 보통 hex 값을 넣거나 정의된 색값(Red, Yellow 등)을 넣습니다. 물론 그라데이션 블러쉬나 Solid Color Brush 로 감싸주는 경우도 있고요.


앞선 포스팅에서도 있었습니다. 바로 아래 코드입니다.

<Ellipse Width="50" Height="50" Fill="Red"></Ellipse>


주의해서 볼 곳은 Fill="Red" 이 부분입니다. Ellipse 클래스를 보면, Fill 프로퍼티의 타입은 Brush 입니다. 그런데 위 코드에서는 문자열(Red)을 넣었습니다. 이게 어떻게 가능한 것일까요?




Test Code


먼저 일반적인 테스트를 위해서 코드를 작성해 보려고 합니다. 약간 억지스러운(?) 코드입니다. 적절한 예를 찾지 못하겠네요 :)

SenseOfTouch 라는 클래스가 있습니다. 생성자는 enum 값인 Sense 를 받습니다. Hot 과 Cold 두가지가 있지요.

그리고 Property(Dependency Property) 로 이 SenseOfTouch 을 갖는 Control 을 하나 만들었습니다. 코드는 아래와 같습니다.



    [SenseOfTouch]


public class SenseOfTouch
{
    public enum Sense { Hot, Cold }
    public Sense Feel { get; set; }

    public SenseOfTouch()
    {
    }


    public SenseOfTouch(Sense sense)
    {
        this.Feel = sense;
    }

    public override string ToString()
    {
        return this.Feel.ToString();
    }
}





    [SenseOfTouchControl.xaml]


<UserControl x:Class="Test.SenseOfTouchControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:test="clr-namespace:Test"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <TextBlock Text="{Binding Feel, RelativeSource={RelativeSource AncestorType=test:SenseOfTouchControl}}"/>
    </Grid>
</UserControl>






    [SenseOfTouchControl.cs]


public partial class SenseOfTouchControl : UserControl
{
    public SenseOfTouch Feel {
        get { return (SenseOfTouch)GetValue(FeelProperty); }
        set { SetValue(FeelProperty, value); }
    }

    // Using a DependencyProperty as the backing store for Feel.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty FeelProperty =
        DependencyProperty.Register("Feel", typeof(SenseOfTouch), typeof(SenseOfTouchControl), new PropertyMetadata(new SenseOfTouch(SenseOfTouch.Sense.Hot)));


    public SenseOfTouchControl() {
        InitializeComponent();
    }
}




복잡해 보이지만, 간단합니다. SenseOfTouch 라는 Control 이 있는데, 프로퍼티로 SenseOfTouch 가 있고, 이 값을 화면에 출력하는게 전부입니다.


<TextBlock Text="{Binding Feel, RelativeSource={RelativeSource AncestorType=test:SenseOfTouchControl}}"/>


이 부분은 프로퍼티의 내용을 TextBlock 에 뿌리겠다라는 건데, Binding 이라고 합니다. 다음에 설명토록 하겠습니다. 지금은 그냥 넘어갑니다.


이제 이 SenseOfTouchControl 을 MainWindow 에 붙여넣고 실행해보도록 하겠습니다.



    [MainWindow]


<Window x:Class="Test.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:test="clr-namespace:Test"
        Title="MainWindow" Height="100" Width="200">
    <Grid>
        <test:SenseOfTouchControl></test:SenseOfTouchControl>
    </Grid>
</Window>








Hot 이라고 화면에 출력되네요. 이유는 SenseOfTouchControl 에서 프로퍼티 초기값(Default)를 Hot 으로 주었기 때문입니다.

public static readonly DependencyProperty FeelProperty =

            DependencyProperty.Register("Feel", typeof(SenseOfTouch), typeof(SenseOfTouchControl), new PropertyMetadata(new SenseOfTouch(SenseOfTouch.Sense.Hot)));



이제 MainWindow 에서 SenseOfTouchControl 의 Feel 값을 Cold 로 변경해 봅시다. static 으로 값을 넣으려면, 미리 값이 있어야 합니다.




public class SensesOfTouch
    {
        private static SenseOfTouch _Hot = new SenseOfTouch(SenseOfTouch.Sense.Hot);
        private static SenseOfTouch _Cold = new SenseOfTouch(SenseOfTouch.Sense.Cold);

        public static SenseOfTouch Hot
        {
            get { return _Hot; }
        }
        public static SenseOfTouch Cold {
            get { return _Cold; }
        }
    }



이렇게 만들어 두고,


    [MainWindow.xaml]


<Window x:Class="Test.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:test="clr-namespace:Test"
        Title="MainWindow" Height="100" Width="200">
    <Grid>
        <test:SenseOfTouchControl Feel="{x:Static test:SensesOfTouch.Cold}"></test:SenseOfTouchControl>
    </Grid>
</Window>




이렇게 프로퍼티 값을 넣어주면 됩니다. Color 를 예로 들자면 이런 셈이지요.



<Ellipse Fill="{x:Static Colors.Red}"></Ellipse>



만일 우리도 Fill 이 "Red" 와 같이 문자열도 값으로 받도록 하려면 어떻게 해야 될까요? 무작정 해 보죠.



<test:SenseOfTouchControl Feel="Cold"></test:SenseOfTouchControl>



잘 되나요? 안타깝게도 빌드가 되지 않습니다.




TypeConverter


지금 시점에 우리에게 필요한것은 바로 TypeConverter 입니다. Color 나 Width / Height 의 Length 정보등이 xaml 에서 문자열을 값으로 받을 수 있는 이유가 바로 이것에 있습니다. xaml 에서 어떤 타입(대게 문자열)을 받았을 때, 이를 원하는 값으로 바꿀수 있게 해 주는 것이죠.


SenseOfTouch 클래스에 TypeConverter 라는 Attribute 를 추가해 줍니다. 짐작하시겠지만, 해당 클래스의 값을 변환할 Converter 를 지정해 주는 것입니다. 더불어 편의를 위해서 Parse 메소드도 추가해 주었습니다.



    [SenseOfTouch]


[TypeConverter(typeof(SenseOfTouchTypeConverter))]
public class SenseOfTouch
{
    public enum Sense { Hot, Cold }
    public Sense Feel { get; set; }

    public SenseOfTouch()
    {
    }


    public SenseOfTouch(Sense sense)
    {
        this.Feel = sense;
    }


    public static SenseOfTouch Parse(string feel)
    {
        if (string.IsNullOrEmpty(feel) == false)
        {
            return new SenseOfTouch((Sense)Enum.Parse(typeof(Sense), feel));
        }
        throw new FormatException("Cannot parse.");
    }

    public override string ToString()
    {
        return this.Feel.ToString();
    }
}



이제 TypeConverter 를 작성해 보도록 하겠습니다. override 해야할 메소드를 보면 ConvertTo 와 ConvertFrom 이 있습니다. 그리고 각각이 가능한지 Can 이 붙은 CanConverTo / CanConvertFrom 이 있습니다. From 은 xaml 에 넣은 값으로 해당 Object 를 만드는 것이고, To 는 이 반대입니다.



    [SenseOfTouchTypeConverter.cs]


public class SenseOfTouchTypeConverter : TypeConverter
{
    public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) {
        if (sourceType == typeof(string)) { return true; }
        return base.CanConvertFrom(context, sourceType);
    }

    public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) {
        if (destinationType == typeof(string)) { return true; }
        return base.CanConvertTo(context, destinationType);
    }

    public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value) {
        string text = value as string;
        if(text != null)
        {
            return SenseOfTouch.Parse(text);
        }
        return base.ConvertFrom(context, culture, value);
    }

    public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
    {
        if (destinationType == null) { throw new ArgumentNullException("destination Type is null"); }

        SenseOfTouch sense = value as SenseOfTouch;
        if(sense != null && this.CanConvertTo(context, destinationType))
        {
            return sense.ToString();
        }

        return base.ConvertTo(context, culture, value, destinationType);
    }
}




이제 모든 준비가 끝났습니다. 이제 Feel 의 값으로 문자열(Cold 또는 Hot)을 넣을 수 있게 되었습니다.


    [MainWindow]


<Window x:Class="Test.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:test="clr-namespace:Test"
        Title="MainWindow" Height="100" Width="200">
    <Grid>
        <test:SenseOfTouchControl Feel="Cold"></test:SenseOfTouchControl>
    </Grid>
</Window>










확인


한번 Ellipse 의 Fill 을 살펴보죠. 정말 TypeConverter 가 있는지, 그리고 어떻게 생겼는지.



    [Ellipse 의 Fill 프로퍼티]


public Brush Fill
        {
            get { return (Brush) base.GetValue(FillProperty); }
            set { base.SetValue(FillProperty, value); }
        }



Fill 의 타입은 Brush 군요. 그럼 Brush 를 살펴보죠. TypeConverter 가 선언되어 있을 겁니다.


    [Brush.cs ]


[Localizability(LocalizationCategory.None, Readability=Readability.Unreadable), TypeConverter(typeof(BrushConverter)), ValueSerializer(typeof(BrushValueSerializer))]
    public abstract class Brush : Animatable, IFormattable, DUCE.IResource
    {
// 생략



Brush 의 TypeConverter  로 BrushConverter 가 정의되어 있네요. 살펴보죠



    [BrushConverter.cs]


public sealed class BrushConverter : TypeConverter
    {
        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            return ((sourceType == typeof(string)) || base.CanConvertFrom(context, sourceType));
        }
        
        public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
        {
            if (!(destinationType == typeof(string)))
            {
                return base.CanConvertTo(context, destinationType);
            }
            if ((context == null) || (context.Instance == null))
            {
                return true;
            }
            if (!(context.Instance is Brush))
            {
                throw new ArgumentException(MS.Internal.PresentationCore.SR.Get("General_Expected_Type", new object[] { "Brush" }), "context");
            }
            Brush instance = (Brush) context.Instance;
            return instance.CanSerializeToString();
        }
        
        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
        {
            if (value == null)
            {
                throw base.GetConvertFromException(value);
            }
            string str = value as string;
            if (str != null)
            {
                return Brush.Parse(str, context);
            }
            return base.ConvertFrom(context, culture, value);
        }
        
        public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
        {
            if ((destinationType != null) && (value is Brush))
            {
                Brush brush = (Brush) value;
                if (destinationType == typeof(string))
                {
                    if (((context != null) && (context.Instance != null)) && !brush.CanSerializeToString())
                    {
                        throw new NotSupportedException(MS.Internal.PresentationCore.SR.Get("Converter_ConvertToNotSupported"));
                    }
                    return brush.ConvertToString(null, culture);
                }
            }
            return base.ConvertTo(context, culture, value, destinationType);
        }
    }




Brush 이외에도 수 많은 클래스들이 Converter 를 가지고 있는 것을 한번 직접 확인해 보시는것도 좋을 것 같습니다.





'Microsoft > WPF' 카테고리의 다른 글

MEF(Managed Extensibility Framework)  (2) 2014.05.19
화면에 노출되는 프로퍼티는?  (1) 2014.05.13
XAML 의 문법구조  (0) 2014.05.12
xaml 과 cs 파일의 관계  (1) 2014.05.08
WPF 기본 동작구조  (0) 2014.05.08