1

Тема: Создание нового Контрола при помощи Шаблона

Создание нового Контрола при помощи Шаблона

перевод документа: Creating a New Control by Creating a ControlTemplate

Замечание:
В тексте используются эквивалентные понятия Шаблон Контрола и его оригинальное написание ControlTempalte.

Silverlight позволяет создавать контролы с настраиваемым внешним видом. Например, вы можете менять вид контрола CheckBox гораздо свободнее, чем позволяют это сделать его стандартные свойства. Для этого вы должны использовать ControlTemplate (шаблон контрола). В следующей демонстрации вы можете увидеть разницу между элментом CheckBox со стандартным ControlTemplate и CheckBox с пользовательским ControlTemplate.

Смотреть пример

Если при создании нового контрола вы соблюдаете модель «Частей и Состояний», тогда вид вашего контрола будет настраиваемым. Дизайнерский инструмент Microsoft Expression Blend поддерживает модель «Частей и Состояний», поэтому если в контроле соблюдена эта модель, то его можно будет настраивать, используя подобные дизайнерские инструменты. В этом разделе обсуждается модель «Частей и Состояний» и как ее придерживаться при создании нового контрола. Здесь будет использован пример создания пользовательского контрола NumericUpDown, чтобы проиллюстрировать идею, заложенную в эту модель. Контрол NumericUpDow показывает числовое значение, которое может быть увеличено или уменьшено путем нажатия на соответствующие кнопки контрола. Вот демонстрация контрола NumericUpDown:

Смотреть пример


Данный раздел состоит из следующих частей:

•    Предварительные оговорки
•    Модель «Частей и Состояний»
•    Создание Визуальной структуры и Визуального поведения контрола
•    Использование «Частей» в коде приложения
•    Создание Контракта контрола
•    Похожие разделы

Thumbs up Thumbs down

2

Re: Создание нового Контрола при помощи Шаблона

Предварительные оговорки

Данное руководство предполагает, что вы уже знаете, как создавать ControlTemplate для уже имеющегося контрола, хорошо знакомы с тем, что такое элементы соответствующие контракту контрола, и понимаете идеи, обсуждаемые в разделе «Настройка вида Контрола через Шаблон Контрола».

Замечание:
Для создания контрола с возможностью настройки его вида вы должны создать наследника класса Control или одного из его подклассов, за исключением класса UserControl. Контрол наследуемый от UserControl предназначен для быстрой разработки, но он не использует ControlTemplate, поэтому вы не можете изменять его вид через ControlTemplate.

Thumbs up Thumbs down

3

Re: Создание нового Контрола при помощи Шаблона

Модель «Частей и Состояний»

Модель частей и состояний оговаривает то, как должна быть описана визуальная структура и визуальное поведение контрола. Чтобы придерживаться этой модели вы должны следовать следующим рекомендациям:

•    Описывать визуальную структуру и визуальное поведение в шаблоне контрола (ControlTemplate).
•    Придерживаться определенных правил при взаимодействии логики контрола с частями шаблона.
•    Предоставлять контракт контрола, чтобы было четко видно, что должен включать в себя ControlTemplate.

Описывая визуальную структуру и визуальное поведение шаблона контрола вы позволяете другим разработчикам изменять визуальную структуру и визуальное поведение вашего контрола через ControlTemplate без необходимости написания кода. Вы должны предоставить контракт, который укажет разработчикам какие объекты класса FrameworkElement (части) и какие состояния должны быть описаны в их ControlTemplate. Вы должны следовать определенным сложившимся правилам, когда вы взаимодействуйте с частями ControlTemplate так, чтобы ваш контрол мог корректно работать даже с неполным ControlTemplate. Если вы следуете этим трем принципам, то для разработчиков процесс создания своего ControlTemplate будет таким же простым, как и для поставляемых в Silverlight стандартных контролов. В следующем разделе вы найдете детальное разъяснение для каждой из даваемых здесь рекомендаций.

Thumbs up Thumbs down

4

Re: Создание нового Контрола при помощи Шаблона

Описание Визуальной структуры и Визуального поведения контрола в ControlTemplate

При создании пользовательского контрола с использованием модели частей и состояний, вы описываете визуальную структуру и визуальное поведение в ControlTemplate, а не в коде отвечающего за его логику. Визуальная структура контрола является композицией объектов класса FrameworkElements (частей), из которых состоит контрол. Визуальное поведение – это то, как выглядит контрол, находясь в определенном состоянии. За подробной информацией о создании шаблона контрола определяющего визуальную структуру и поведение ознакомьтесь с разделом Настройка вида Контрола через Шаблон Контрола.

В примере с контролом NumericUpDown визуальная структура включает два элемента RepeatButton и один TextBlock. Если вы создадите эти элементы в коде контрола NumericUpDown, например, в конструкторе класса, тогда уже не будет возможности изменить их координаты в контроле. Вместо того чтобы задавать структуру вида и поведение вида этого контрола программно, вы должны описать ее в ControlTemplate. Тогда другие разработчики смогут настроить позицию размещения кнопок и тестового поля, а также изменить поведение при отрицательном числе счетчика, и все благодаря тому, что вы предусмотрели возможность настройки контрола через ControlTemplate.

В следующем примере вы видите структуру вида контрола NumericUpDown, в которую включены следующие элементы: RepeatButton для увеличение значения счетчика, RepeatButton для уменьшения значения, а также TextBlock для отображения этого значения.

<ControlTemplate TargetType="src:NumericUpDown">
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition/>
      <RowDefinition/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
      <ColumnDefinition/>
      <ColumnDefinition/>
    </Grid.ColumnDefinitions>

      <Border BorderThickness="1" BorderBrush="Gray" 
            Margin="7,2,2,2" Grid.RowSpan="2" 
            Background="#E0FFFFFF"
            VerticalAlignment="Center" 
            HorizontalAlignment="Stretch">
        <TextBlock x:Name="TextBlock" TextAlignment="Center" Padding="5"
             Foreground="{TemplateBinding Foreground}"/>

      </Border>
    <RepeatButton Content="Up" Margin="2,5,5,0"
                  x:Name="UpButton"
                  Grid.Column="1" Grid.Row="0"
                  />
    <RepeatButton Content="Down" Margin="2,0,5,5"
                  x:Name="DownButton"
                  Grid.Column="1" Grid.Row="1"/>
  </Grid>

Визуальное поведение контрола NumericUpDown делает шрифт красным, если значение поля меньше нуля. Если бы при отрицательном значении поля вы бы изменяли свойство Foregorund элемента TextBlock непосредственно в коде, тогда контрол NumericUpDown всегда бы отображал красный текст при отрицательном значении, и это невозможно было бы изменить через шаблон. Вы определяете визуальное поведение контрола в ControlTemplate путем добавления объекта VisualState. Следующий пример демонстрирует объекты VisualState для положительного (Positive) и отрицательного (Negative) состояний. Состояния Positive и Negative взаимоисключающие (контрол может находиться только в одном из двух), поэтому в примере эти VisualState-объекты находятся в одной группе VisualStateGroup. Когда контрол находится  в состоянии Negative, свойство Foreground элемента TextBlock делается красным. А когда контрол находится в состоянии Positive, свойство Foreground возвращается к исходному цвету. Описание объектов VisualState для ControlTemplate будет обсуждаться в разделе Настройка вида Контрола через Шаблон Контрола.

<vsm:VisualStateGroup x:Name="ValueStates">

  <!--Make the Value property red when it is negative.-->
  <vsm:VisualState x:Name="Negative">
    <Storyboard>
      <ColorAnimation To="Red"
                      Storyboard.TargetName="TextBlock" 
                      Storyboard.TargetProperty="(Foreground).(SolidBruch.Color)"/>
    </Storyboard>

  </vsm:VisualState>

  <!--Return the control to its initial state by
      return the TextBlock's Foreground to its 
      original color.-->
  <vsm:VisualState x:Name="Positive"/>
</vsm:VisualStateGroup>

Thumbs up Thumbs down

5

Re: Создание нового Контрола при помощи Шаблона

Использование Частей Шаблона Контрола в коде

Автор шаблона контрола может пропустить необходимые объекты FrameworkElement или VisualState либо намеренно, либо по невнимательности. Однако корректная работа вашего контрола может зависеть от этих частей. Модель частей и состояний предписывает вашему контролу быть гибким, а значит продолжать работать, даже если необходимые ему объекты FrameworkElement или VisualState в данном ControlTemplate отсутствуют. Т.е. ваш контрол не должен выбрасывать исключение или выводить сообщение об ошибке, если какой-то FrameworkElement,  VisualState или VisaulStateGroup не найдены в ControlTemplate. В этой части мы поговорим о рекомендованных правилах взаимодействия объектов FrameworkElement и об управлении состояниями.


Что делать, если необходимые объекты FrameworkElement отстуствуют

То, что вы описываете объекты FrameworkElement в ControlTemplate, означает, что логика кода вашего контрола может взаимодействовать с некоторыми из этих объектов. Например, контрол NumericUpDown подписывается на событие кнопки Click, чтобы увеличивать или уменьшать значение поля, а также, чтобы изменять свойство Text элемента TextBlock в соответствии с этим значением. Если в ControlTempalte пропущен элемент TextBlock или Button, вполне приемлемо, что контрол потеряет часть своей функциональности, но вы должны гарантировать, что в работе вашего контрола не возникнет никакой ошибки. Например, если ControlTemplate не содержит кнопки для изменения значения поля, тогда контрол NumericUpDown утратит часть своего функционала, но приложение, которое будет его использовать, продолжит работать без сбоев.

Следующие правила обеспечат должную реакцию вашего контрола на отсутствующие объекты FrameworkElement:

1.    Указывайте атрибут x:Name для каждого объекта FrameworkElement, на который вы будете ссылаться из кода.
2.    Создайте закрытые свойства для каждого объекта FrameworkElement, с которым вы будете взаимодействоать.
3.    Если ваш контрол обрабатывает какие-либо события объектов FrameworkElement, подписывайтесь и отписывайтесь от них в set-аксессоре свойства соответствующего объекта FrameworkElement.
4.    Инициализацию свойств описанных в шаге 2 производите в методе OnApplyTemplate. Этот метод вызывает раньше всех других, в которых может быть получен доступ к ControlTemplate. Для обращения к объектам FrameworkElement используйте свойство x:Name.
5.    Перед обращением к членам объекта FrameworkElement, убедитесь, что он не равен null. Если он равен null, не сообщайте об ошибке.

Следующий пример демонстрирует взаимодействие контрола NumericUpDown с объектами FrameworkElement в соответствии с уже известным списком рекомендаций.

В примере XAML описывающем визуальную структуру контрола NumericUpDown в ControlTemplate, в кнопке RepeatButton, увеличивающей значение поля, атрибуту x:Name присвоено имя «UpButton». В следующем примере свойство UpButtonElement предстваляет кнопку RepeatButton, которая объявлена в ControlTemplate. Аксессор set в первую очередь, если UpDownElement не равен null, отписывается от события кнопки Click, а затем подписывается на это же событие Click. Другие других элементов: свойство DownButtonElement элемента RepeatButton, и свойство TextProperties элемента TextBlock в данном примере опущены.

private RepeatButton upButtonElement;

private RepeatButton UpButtonElement
{
    get
    {
        return upButtonElement;
    }

    set
    {
        if (upButtonElement != null)
        {
            upButtonElement.Click -=
                new RoutedEventHandler(upButtonElement_Click);
        }
        upButtonElement = value;

        if (upButtonElement != null)
        {
            upButtonElement.Click +=
                new RoutedEventHandler(upButtonElement_Click);
        }
    }
}

Следующий пример демонстрирует метод OnApplyTemplate. В примере для извлечения объектов из ControlTemplate используется метод GetTemplateChild. Обратите внимание, что в примере предусмотрена защита от случаев, когда метод GetTemplateChild находит объект FrameworkElement с указанным именем, но неправильного типа. Игнорирование элементов неправильного типа также относится к рекомендованным правилам.

public override void OnApplyTemplate()
{
    UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
    DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
    TextElement = GetTemplateChild("TextBlock") as TextBlock;

    UpdateStates(false);
}

Когда свойства Value контрола NumericUpDown изменяется, контрол должен обновить свойство Text элемента TextBlock. В следующем примере показан фрагмент функции обратного вызова ValueChangedCallback срабатывающей при изменении значения зависимого свойства Value. В примере перед обращением к свойству Text элемента TextBlock проверяется, не равен ли TextBlock null.

        private static void ValueChangedCallback(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            NumericUpDown ctl = (NumericUpDown)obj;
            int newValue = (int)args.NewValue;

            // Update the TextElement to the new value.
            if (ctl.TextElement != null)
            {
                ctl.TextElement.Text = newValue.ToString();
            }



...


        }

Согласно правилам, показанным в предыдущем примере, вы гарантируете, что ваш контрол продолжит работать, даже если ControlTemplate не содержит необходимый объект FrameworkElement.

Thumbs up Thumbs down

6

Re: Создание нового Контрола при помощи Шаблона

Используйте VisualStateManager для управления состояниями

VisualStateManager отслеживает состояия контрола и отвечает за исполнение логики переходов между состояниями. Когда вы добавляете в ControlTemplate объекты VisualState, вы добавляете их в секцию VisualStateGroup, которая в свою очередь описывается в присоединяемом свойстве VisualStateManger.VisualStateGroups, поэтому VisualStateManager имеет доступ ко всем этим объектам.

Следующий пример повторяет один из предыдущих примеров, демонстрирующих объекты контрола VisualState, соответствующие состояниям Positive и Negative. Storyboard объекта VisualState состояния Negative изменяет свойства цвета Foregorund элемента TextBlock на красный. Когда контрол NumericUpDown находится в состоянии Negative, срабатывает Storyboard этого состояния. А когда контрол возвращается в состояние Positive, Storyboard прекращает действовать. Для Positive VisualState нет нужды описывать Storyboard, поскольку когда Storyboard в состоянии Negative прекращает работать, свойство цвета Foregorund возвращается в своё исходное значение.

<vsm:VisualStateGroup x:Name="ValueStates">

  <!--Make the Value property red when it is negative.-->
  <vsm:VisualState x:Name="Negative">
    <Storyboard>
      <ColorAnimation To="Red"
                      Storyboard.TargetName="TextBlock" 
                      Storyboard.TargetProperty="(Foreground).(SolidBruch.Color)"/>
    </Storyboard>

  </vsm:VisualState>

  <!--Свойство Foreground получает своё исходное значение.-->
  <vsm:VisualState x:Name="Positive"/>
</vsm:VisualStateGroup>

Логика контрола отвечает за изменение его состояния. В следующем примере показано, что контрол NumericUpDown вызывает метод GoToState для перехода в состояния Positive, когда Value больше или равно 0, и в состояние Negative, когда Value меньше 0.

if (Value >= 0)
{
    VisualStateManager.GoToState(this, "Positive", useTransitions);
}
else
{
    VisualStateManager.GoToState(this, "Negative", useTransitions);
}

Метод GoToState отвечает за логику необходимую для активации и остановки соответствующих объектов Storyboard. Когда контрол вызывает метод GoToState чтобы изменить свое состояние, VisualStateManger делает следующее:

•    Если VisualState, к которому собирается перейти контрол, содержит Storyboard, тогда данный Storyboard начинает работу. Если VisualState, в котором контрол находился перед началом перехода, содержит Storyboard, работа этого Storyboard прекращается.

•    Если контрол уже находится в нужном состоянии, тогда GoToState не производит никаких действий и возвращает true.

•    Если требуемое состояние отсутствует в ControlTempalte, тогда GoToSate не производит никаких действий и возвращает false.


Рекомендованная модель работы с VisualStateManger

Для того, чтобы поддерживать состояния вашего контрола рекомендуется сделать следующее:

•    Использовать свойства для контроля состояния контрола.

•    Создать вспомогательный метод для перехода между состояниями.


Контрол NumericUpDown использует свойство Value для определения, находится ли он в состоянии Positive или Negative. Помимо состояний Positive и Negative, контрол определяет состояния Focused и UnFocused указывающие, находится ли контрол в фокусе или нет. В следующем примере показано, что контрол описывает закрытое свойство isFocused, как признак того, что контрол в фокусе.

bool isFocused;

protected override void OnGotFocus(RoutedEventArgs e)
{
    base.OnGotFocus(e);
    isFocused = true;
    UpdateStates(true);
}

protected override void OnLostFocus(RoutedEventArgs e)
{
    base.OnLostFocus(e);
    isFocused = false;
    UpdateStates(true);
}

В следующем примере вы видите вспомогательный метод контрола UpdateStates. Когда Value больше или равно 0, контрол находится в состоянии Positive. А когда Value меньше 0, контрол находится в состоянии Negative. Если значение свойства isFocused = true, контрол находится в состоянии Focused, в противном случае – Unfocused.

private void UpdateStates(bool useTransitions)
{
    if (Value >= 0)
    {
        VisualStateManager.GoToState(this, "Positive", useTransitions);
    }
    else
    {
        VisualStateManager.GoToState(this, "Negative", useTransitions);
    }

    if (isFocused)
    {
        VisualStateManager.GoToState(this, "Focused", useTransitions);
    }
    else
    {
        VisualStateManager.GoToState(this, "Unfocused", useTransitions);
    }

}

Если в функцию GoToState передать название состояние, в котором контрол уже находится, тогда GoToState сработает в холостую, поэтому вам нет нужды проверять текущее состояние контрола. Например, когда Value содержит отрицательное число, а затем получает другое отрицательное число, работа объекта Storyboard в состоянии Negative не прерывается и пользователь не увидит никаких изменений.

VisualStateManager использует объекты VisualStateGroup для выяснения текущего состояния контрола в момент, когда вы вызываете метод GoToState.  Контрол всегда находится только в одном состоянии из каждой группы VisualStateGroup и, следовательно, при переходе в новое состояние покидает только одно состояние из той же группы VisualStateGroup. Например, шаблон контрола NumericUpDown описывает два состояния Positive и Negative в одной группе VisualStateGroup, и два состояния Focused и Unfocused в другой группе. Когда контрол переходит из состояния Positive в состояние Negative, или наоборот, состояния других групп остаются незатронутыми.

Вот три характерных места, где состояние контрола может измениться:
•    Когда ControlTemplate применяется к контролу при инициализации.
•    Когда изменяются значения свойств.
•    Когда срабатывают события.

В следующих примерах показано как корректируются состояния контрола в этих случаях.

Вы должны корректировать состояние контрола в методе OnApplyTemplate (он вызывается при инициализации шаблона контрола) так, чтобы контрол появился в правильном состоянии. Для этого в методе OnApplyTemplate вызывается метод UpdateStates. Допустим, вы создаете контрол NumericUpDown и затем задаете свойству Foreground значение «зеленый», а свойству Value – значение -5. Если в момент инициализации шаблона контрола вы не вызовете метод UpdateStates, тогда контрол не войдет в состояние Negative, и будет иметь зеленый цвет вместо красного.

Замечание:
В методе OnApplyTemplate метод UpdateStates вызывается с параметром false для того, чтобы при первом появлении контрола не проигрывалась анимация перехода между состояниями.

public override void OnApplyTemplate()
{
    UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
    DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
    TextElement = GetTemplateChild("TextBlock") as TextBlock;

    UpdateStates(false);
}

Вам нередко придется корректировать состояния контрола при изменении значений его свойств. В следующем примере показан целиком метод ValueChangedCallback. Поскольку метод ValueChangedCallback вызывается когда свойство Value получает новое значение, внутри него вызывается метод UpdateStates на случай, если значение Value сменилось от положительного к отрицательному числу или наоборот. Допускается вызывать метод UpdateStates даже когда Value получает новое значение, но с прежним знаком, поскольку в этом случае просто ничего не произойдет.

private static void ValueChangedCallback(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
    NumericUpDown ctl = (NumericUpDown)obj;
    int newValue = (int)args.NewValue;

    // Update the TextElement to the new value.
    if (ctl.TextElement != null)
    {
        ctl.TextElement.Text = newValue.ToString();
    }

    // Call UpdateStates because the Value might have caused the
    // control to change ValueStates.
    ctl.UpdateStates(true);

    // Raise the ValueChanged event so applications can be alerted
    // when Value changes.
    ValueChangedEventArgs e = new ValueChangedEventArgs(newValue);
    ctl.OnValueChanged(e);
}

Кроме того, вам может понадобиться изменить состояние при срабатывании какого-либо события. В следующем примере показано, что контрол вызывает метод UpdateStates в обработчике OnGotFocus.

protected override void OnGotFocus(RoutedEventArgs e)
{
    base.OnGotFocus(e);
    isFocused = true;
    UpdateStates(true);
}

VisualStateManager помогает вам управлять состояниями контрола. С его помощью мы можете гарантировать, что ваш контрол выполняет переходы между состояниями корректно. Если вы соблюдаете рекомендации по работе с VisualStateManger, описанные в данном разделе, в вашем контроле будет проще разобраться другим разработчикам, а также будет легче его сопровождать.

Thumbs up Thumbs down

7

Re: Создание нового Контрола при помощи Шаблона

Подготовка Контракта контрола

Контракт контрола нужен для того, чтобы авторы шаблона контрола могли точно знать, что должен включать в себя ControlTemplate. Контракт контрола состоит из трех элементов:

•    Визуальные элементы, используемые в логике кода контрола.
•    Состояния контрола, а также группы, к которым принадлежит каждое из состояний.
•    Открытые свойства, через которые можно изменять вид контрола.

Тот, кто создает новый ControlTemplate, должен знать, какие объекты FrameworkElement использует логика кода контрола, тип каждого такого объекта, а также его имя. Автор нового шаблона контрола также должен знать названия всех возможных состояний контрола, а также названия групп VisualStateGroup, к которым эти состояния принадлежат.

Уже знакомый вам пример контрола NumericUpDown ожидает найти в ControlTemplate следующие объекты FrameworkElement:

•    RepeatButton с именем UpButton.
•    RepeatButton с именем DownButton.
•    TextBlock с именем TextBlock.


Контрол может быть в следующих состояниях:

•    Группа ValueStates
  o    Positive
  o    Negative
•    Группа FocusStates
  o    Focused
  o    Unfocused

Чтобы указать, какие объекты FrameworkElement должен содержать ControlTemplate, используйте TemplatePartAttribute, который позволяет указать имя и тип объекта. Для указания допустимых состояний контрола используйте TemplateVisualStateAttribute, который позволяет указать название и группу состояния. Элементы спецификации TemplatePartAttribute и TemplateVisualStateAttribute расположите перед определением класса контрола.

Все открытые свойства, влияющие на вид контрола, также являются частью контракта контрола.

В следующем примере показано, как следует описывать спецификацию объектов FrameworkElement и состояний контрола NumericUpDown.

[TemplatePart(Name = "TextElement", Type = typeof(TextBlock))]
[TemplatePart(Name = "UpButtonElement", Type = typeof(RepeatButton))]
[TemplatePart(Name = "DownButtonElement", Type = typeof(RepeatButton))]
[TemplateVisualState(Name = "Positive", GroupName = "ValueStates")]
[TemplateVisualState(Name = "Negative", GroupName = "ValueStates")]
[TemplateVisualState(Name = "Focused", GroupName = "FocusedStates")]
[TemplateVisualState(Name = "Unfocused", GroupName = "FocusedStates")]
public class NumericUpDown : Control
{
    public static readonly DependencyProperty BackgroundProperty;
    public static readonly DependencyProperty BorderBrushProperty;
    public static readonly DependencyProperty BorderThicknessProperty;
    public static readonly DependencyProperty FontFamilyProperty;
    public static readonly DependencyProperty FontSizeProperty;
    public static readonly DependencyProperty FontStretchProperty;
    public static readonly DependencyProperty FontStyleProperty;
    public static readonly DependencyProperty FontWeightProperty;
    public static readonly DependencyProperty ForegroundProperty;
    public static readonly DependencyProperty HorizontalContentAlignmentProperty;
    public static readonly DependencyProperty PaddingProperty;
    public static readonly DependencyProperty TextAlignmentProperty;
    public static readonly DependencyProperty TextDecorationsProperty;
    public static readonly DependencyProperty TextWrappingProperty;
    public static readonly DependencyProperty VerticalContentAlignmentProperty;

    public Brush Background { get; set; }
    public Brush BorderBrush { get; set; }
    public Thickness BorderThickness { get; set; }
    public FontFamily FontFamily { get; set; }
    public double FontSize { get; set; }
    public FontStretch FontStretch { get; set; }
    public FontStyle FontStyle { get; set; }
    public FontWeight FontWeight { get; set; }
    public Brush Foreground { get; set; }
    public HorizontalAlignment HorizontalContentAlignment { get; set; }
    public Thickness Padding { get; set; }
    public TextAlignment TextAlignment { get; set; }
    public TextDecorationCollection TextDecorations { get; set; }
    public TextWrapping TextWrapping { get; set; }
    public VerticalAlignment VerticalContentAlignment { get; set; }
}

Thumbs up Thumbs down