TextBlock에 Binidng된 Value 변경 시 Converter가 동작하지 않는 문제

이 글에 대한 해결책을 얻고자 하신다면 조금 다른 방향으로 시도해 볼 수 있습니다.

WPF에서 사용되는 대부분의 컨트롤(TextBox의 입력 관련 렌더링이나 FlowDocument 내의 문자들 제외)은 TextBlock을 통해 문자를 표출하는데요, 이 TextBlockText 속성에 할당된 문자열을 그리기 위한 내부 객체를 생성하는 부분의 로직을 가로채서 문자열을 바꿔줄 수 있습니다.

이 방법은 무협으로 치면 :fire:사파 무공:fire:에 해당하는 방법으로 실무에 사용하는 것은 추천하지 않습니다. :sweat_smile:
런타임에 이미 컴파일 된 함수를 후킹해서 원하는 기능을 하도록 변경하는 방법으로 정석적인 방법과는 거리가 멀며 잘못 사용할 경우 소프트웨어의 안정성에 큰 영향을 줄 수도 있는 파괴적인 방법입니다.

본론으로 들어가 TextBlockText 속성 값을 어떻게 그리지는 확인하기 위해 dnSpy를 통해 OnRender 함수를 살펴보면, 위에서 언급한 Text 속성을 그리기 위한 내부 객체는 Line이라는 형식의 객체로 CreateLine이라는 함수를 통해 생성됩니다.

해당 함수의 내부 구현은 아래와 같습니다.

TextBlockInline, Run과 같이 복잡한 형식의 하위 항목을 포함하지 않고 단순히 Text 문자열 만을 포함하고 있다면 SimpleLine이라는 객체를 만들며, 이때 생성자에 Text 속성 값을 넘기도록 되어 있습니다.

line = new SimpleLine(this, this.Text, lineProperties.DefaultTextRunProperties);

우리는 이 지점에서 this.Text 부분에 대한 조건 검사를 수행한 뒤 변경된 문자열 값을 넘겨줄 것입니다.

런타임 함수를 후킹하기 위해서 프로젝트에 MonoMod.RuntimeDetour 패키지를 설치해 줍니다.

image

프로그램 진입점인 App 클래스에 다음 내용을 추가해 줍니다.

using System.Reflection;
using System.Windows;
using System.Windows.Controls;

using MonoMod.RuntimeDetour;

public partial class App : Application
{
    private static Hook _hookCreateLine; // Keep hook object from GC 
    private static PropertyInfo _piDefaultTextRunProperties;
    private static ConstructorInfo _ciSimpleLine;

    public App()
    {
        var miCreateLine = typeof(TextBlock).GetMethod(
                "CreateLine", 
                BindingFlags.NonPublic | BindingFlags.Instance);
        _hookCreateLine = new Hook(miCreateLine, CreateLine);

        var tiSimpleLine = typeof(TextBlock).Assembly.DefinedTypes
                .FirstOrDefault(x => x.Name == "SimpleLine");
        _ciSimpleLine = tiSimpleLine.DeclaredConstructors.FirstOrDefault();
    }

    private static object CreateLine(
        Func<TextBlock, object, object> orig, TextBlock self,
        object lineProperty)
    {
        if (_piDefaultTextRunProperties == null)
        {
            _piDefaultTextRunProperties = 
                lineProperty.GetType().GetProperty("DefaultTextRunProperties");
        }

        if (self.Text == "A") // 조건 검사, 
        {
            var trp = _piDefaultTextRunProperties.GetValue(lineProperty);
            var replaced = "A11"; // 변경된 값
            // SimpleLine 생성 후 리턴
            return _ciSimpleLine.Invoke(new object[] { self, replaced, trp }); 
        }
        else // 조건에 부합하지 않으면 원본 메서드를 호출
        {
            return orig(self, lineProperty);
        }
    }
}

아래와 같은 결과를 얻으실 수 있습니다.

image

관련 패키지에 대해서는 아래 링크를 참고하시면 도움이 될 듯 합니다.

추가적으로 위와 같은 변경이 필요한 컨트롤은 후킹 지점을 직접 찾아 구현해 보시기 바랍니다.

MonoMod/MonoMod.UnitTest/RuntimeDetour/HookTest.cs at master · MonoMod/MonoMod (github.com)

2개의 좋아요