WPF: Стили, изменение содержимого переопределенного Content
Написал стиль для кнопок моего приложения, который вместо обычного текста выводит вначале иконку, а за ним уже текстовый блок. Вот он:
<Setter Property="Content">
<Setter.Value>
<StackPanel Orientation="Horizontal">
<Image Width="16" Height="16" Stretch="Fill" Margin="3,0,3,0" Source="{Binding}"/>
<TextBlock Text="{Binding}" Margin="3,0,3,0"/>
</StackPanel>
</Setter.Value>
</Setter>
</Style>
А теперь, собственно, вопрос - как создать кнопку, использующую этот стиль, и задать ей значения Text и Image Source? {Binding} тут вставлен на обум - подсмотрен в чьем-то примере. Или стили для этого не подходят и мне нужно создавать собственный класс кнопки, наследуясь от Button, добавлять в него новые свойства и встроенный стиль?
Помогите, гуру!
Заранее спасибо.
---
Нарисовал такое вот чудовище:
<Grid>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
<ContentPresenter Width="16" Height="16" Content="{TemplateBinding Tag}"/>
<TextBlock Text="{TemplateBinding Content}" Margin="3,0,3,0"/>
</StackPanel>
</Grid>
</ControlTemplate>
ContentPresenter, который нужен только для вывода передаваемого Image'а, биндинг по полю Tag... В общем, все страшно. Вопрос - как это облагородить? Как делать правильно и оптимально?
Никто не мешает наследоваться от класса Button, либо от ButtonBase, добавив необходимые зависимые свойства.
Если изображение зависит от контекста данных (DataContext) то, конечно, наследование не нужно.
ContentPresenter, который нужен только для вывода передаваемого Image'а, биндинг по полю Tag... В общем, все страшно. Вопрос - как это облагородить? Как делать правильно и оптимально?
Я бы создал свой класс кнопки (на основе ButtonBase), прикрутив к нему стиль, основанный на стандартном.
---
Хм, возникла проблема. Тут. конечно, наверняка мои руки, так как я еще слабо ориентируюсь в системе нэймспейсов WPF, но тем не менее...
Создал в Solution Explorer папочку Controls. Добавил новый класс - ImageButton. Унаследовал его от Button. В его XAML изменил теги <UserControl> на <Button>. Внешний вид пустого прямоугольника изменился.
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"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
</Grid>
</Button>
В ресурсном словаре, содержащем стили кнопок создаю DataTemplate для своего контрола, подключив соответствующее пространство имен:
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:XNARPGEditor.Controls">
...
<ControlTemplate x:Key="iconButtonTemplate" TargetType="{x:Type c:ImageButton}">
Компиляция прохоидт на ура, но при запуске получаю следующую ошибку:
"Не удалось создать "Type" на основе текста "c:ImageButton".": номер строки "116" и позиция в строке "49".
Заменяю на:
Получаю наналогичную ошибку:
"Не удалось создать "TargetType" на основе текста "XNARPGEditor.Controls.ImageButton".": номер строки "116" и позиция в строке "49".
В чем проблема? Везде рассматриваются какие-то элементарные ситуации, когда вся эта пакость лежит в одном пространстве имен. >_>
Создал в Solution Explorer папочку Controls. Добавил новый класс - ImageButton. Унаследовал его от Button. В его XAML изменил теги <UserControl> на <Button>.
Вот это было зря. Нужно следующим образом:
1) Создаем класс, наслеованный от Button:
{
public class ImageButton : Button { }
}
2) Создаем ResourceDictionary-файл Themes\Generic.xaml, ставим ему тип Embedded Resource
В этом файле создаем стиль:
<Style TargetType="local:ImageButton">
<!-- тут и будет стиль кнопки -->
</Style>
..
В принципе, можно подключить этот стиль обычным образом - через App.xaml.
Писал в браузере, надеюсь поможет.
Ок. Вот шаги создания собственного контрола:
1) Создаем класс MyCustomButton
namespace MyWpfApplication.Controls
{
public class MyCustomButton : ButtonBase
{
}
}
2) Создаем стиль Style\MyCustomControl.xaml, ставим ему Build Action = Resource:
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MyWpfApplication.Controls;assembly=MyWpfApplication"> <!-- ВАЖНО: имя сборки указывать нужно! -->
<Style TargetType="{x:Type local:MyCustomButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:MyCustomButton}">
<Border Background="Red" BorderBrush="Bisque" BorderThickness="2">
<ContentPresenter Content="{TemplateBinding Content}" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
3) В App.xaml дописываем ссылку на ресурс:
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="Window1.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Styles\MyCustomButtonStyle.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
4) Используем
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MyWpfApplication.Controls"
Title="Window1" Height="300" Width="300">
<Grid>
<local:MyCustomButton Content="Text" />
</Grid>
</Window>
P.S. Я с WPF дела не имею, занимаясь лишь Silverlight-ом.
---
О! Получен ответ. Я попутно связался с Романом Здебским и задал тот же вопрос. Он предложил то же решение, но заодно познакомил с ведущим экспертом MVP по WPF - Александром Шером. И на наши размышлизмы он ответил так:
Сейчас попробую реализовать. Но в любом случае, hardcase, тебе еще раз огромное спасибо - на будущее буду знать, как делать собственные контролы! :)
---
Для Silverilight тут:
http://silverlight.su/viewtopic.php?id=223
Надеюсь, в WPF аналогичным образом...
Да, про аттачнутые свойства я-то и забыл. Они в исходной задаче в самый раз подходят. :)
---
Аха! Нашел более полную информацию:
в System.Windows.DependencyProperty.ValidateDefaultValueCommon(Object defaultValue, Type propertyType, String propertyName, ValidateValueCallback validateValueCallback, Boolean checkThreadAffinity)
в System.Windows.DependencyProperty.RegisterCommon(String name, Type propertyType, Type ownerType, PropertyMetadata defaultMetadata, ValidateValueCallback validateValueCallback)
в System.Windows.DependencyProperty.RegisterAttached(String name, Type propertyType, Type ownerType, PropertyMetadata defaultMetadata)
в XNARPGEditor.AttachedProperties.Button..cctor() в D:\Coding\C#\XNA\XNARPGEditor\Attached Properties\Button.cs:строка 12
---
Даааааааааааааа!!! Работает хреновина! ^_^ Ура! Щаз такого наворочу!..
---
Потомкам:
{
public static class Button
{
public static readonly DependencyProperty IconProperty =
DependencyProperty.RegisterAttached(
"Icon",
typeof(Image),
typeof(FrameworkElement),
new PropertyMetadata(null)); // вот здесь я забывал выставить значение по-умолчанию
[AttachedPropertyBrowsableForType(typeof(Control))]
public static Image GetIcon(FrameworkElement ctl)
{
return (Image)ctl.GetValue(IconProperty);
}
public static void SetIcon(FrameworkElement ctl, Image value)
{
ctl.SetValue(IconProperty, value);
}
}
}
В WPF вполне осмысленные сообщения, а вот в Silverlight полный мрак с этим - приходится зачастую полагаться на телепатию.
Заинтриговал! Надо будет обязательно попробовать! :)
Вот немного видоизменившийся класс, описывающий свойство расширения:
using System.Windows.Controls;
using System.Windows.Media.Imaging;
namespace AttachedProperties
{
public class Button
{
public static readonly DependencyProperty IconProperty =
DependencyProperty.RegisterAttached(
"Icon",
typeof(BitmapImage),
typeof(FrameworkElement),
new PropertyMetadata(null));
[AttachedPropertyBrowsableForType(typeof(Control))]
public static BitmapImage GetIcon(FrameworkElement ctl)
{
return (BitmapImage)ctl.GetValue(IconProperty);
}
public static void SetIcon(FrameworkElement ctl, BitmapImage value)
{
ctl.SetValue(IconProperty, value);
}
}
}
Окошко, в котором разворачивается действо:
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:res="clr-namespace:XNARPGEditor.Properties"
xmlns:local="clr-namespace:XNARPGEditor"
xmlns:ap="clr-namespace:AttachedProperties;assembly=XNARPGEditor"
Title="Characteristics" Height="304" Width="800" Loaded="Window_Loaded">
<Grid>
<Button Style="{StaticResource ResourceKey=IconButtonStyle}" ap:Button.Icon="{StaticResource ResourceKey=iconOkGreen}" Height="23" Margin="5" Name="acceptButton" Grid.Column="0" Click="acceptButton_Click" Content="{x:Static res:Resources.uAccept}"/>
</Grid>
</Window>
Картинка описывается в App.xml:
И, как я уже говорил, и в студии, и в блендере все корректно отображается, компилируется без проблем. Но при запуске - ошибка.
Что интересно:
если я в Window (в стилях работает нормально), укажу сборку, то, при попытке комплияции, получу ошибку:
Я так понимаю, проблема именно в этом... но никак не могу разобраться - почему...
Собственно, само исключение:
Message=Не удалось создать "DependencyProperty" на основе текста "ap:Button.Icon".
Source=PresentationFramework
LineNumber=0
LinePosition=0
StackTrace:
в System.Windows.FrameworkTemplate.LoadTemplateXaml(XamlReader templateReader, XamlObjectWriter currentWriter)
в System.Windows.FrameworkTemplate.LoadTemplateXaml(XamlObjectWriter objectWriter)
в System.Windows.FrameworkTemplate.LoadOptimizedTemplateContent(DependencyObject container, IComponentConnector componentConnector, IStyleConnector styleConnector, List`1 affectedChildren, UncommonField`1 templatedNonFeChildrenField)
в System.Windows.FrameworkTemplate.LoadContent(DependencyObject container, List`1 affectedChildren)
в System.Windows.StyleHelper.ApplyTemplateContent(UncommonField`1 dataField, DependencyObject container, FrameworkElementFactory templateRoot, Int32 lastChildIndex, HybridDictionary childIndexFromChildID, FrameworkTemplate frameworkTemplate)
в System.Windows.FrameworkTemplate.ApplyTemplateContent(UncommonField`1 templateDataField, FrameworkElement container)
в System.Windows.FrameworkElement.ApplyTemplate()
в System.Windows.FrameworkElement.MeasureCore(Size availableSize)
...
в MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)
в MS.Win32.UnsafeNativeMethods.DispatchMessage(MSG& msg)
в System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame)
в System.Windows.Application.RunInternal(Window window)
в System.Windows.Application.Run()
в XNARPGEditor.App.Main() в D:\Coding\C#\XNA\XNARPGEditor\obj\x86\Debug\App.g.cs:строка 0
в System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
в Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
в System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean ignoreSyncCtx)
в System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
в System.Threading.ThreadHelper.ThreadStart()
InnerException: System.NotSupportedException
Message=DependencyPropertyConverter не может выполнить преобразование из System.String.
Source=System
StackTrace:
в System.ComponentModel.TypeConverter.GetConvertFromException(Object value)
в System.Windows.Markup.DependencyPropertyConverter.ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, Object source)
в MS.Internal.Xaml.Runtime.ClrObjectRuntime.CreateObjectWithTypeConverter(ServiceProviderContext serviceContext, XamlValueConverter`1 ts, Object value)
в System.Xaml.XamlObjectWriter.Logic_CreateFromValue(ObjectWriterContext ctx, XamlValueConverter`1 typeConverter, Object value, XamlMember property, String targetName, IAddLineInfo lineInfo)
InnerException:
...только сейчас заметил затерявшуюся в дебрях строчку:
Но я не понимаю - к чему это! Где он нашел эти несчастные строки?!
---
Ах да, если в стилях закомментировать вывод изображения - все работает на ура! Вот тело шаблона:
<Image Source="{TemplateBinding ap:Button.Icon}"/>
<ContentPresenter Margin="2" HorizontalAlignment="Center" VerticalAlignment="Center" RecognizesAccessKey="True" />
</StackPanel>
{
public static readonly DependencyProperty IconProperty =
DependencyProperty.RegisterAttached(
"Icon",
typeof(BitmapImage),
typeof(Buttons), // вот здесь была ошибка
new PropertyMetadata(null));
[AttachedPropertyBrowsableForType(typeof(Control))]
public static BitmapImage GetIcon(UIElement ctl)
{
return (BitmapImage)ctl.GetValue(IconProperty);
}
public static void SetIcon(UIElement ctl, BitmapImage value)
{
ctl.SetValue(IconProperty, value);
}
}
Осознание выдержки из MSDN:
Тип: System.Type
Тип владельца, регистрирующего свойство зависимостей.
Пришло, как обычно, уже после. Нужно указывать класс, регистриующий свойство, а я указывал класс кнопки.
Button сам по себе поддерживает вложенность элементов. Почему просто не написать вот так:
<Button Click="btnAdd" Height="20" >
<Grid>
<Image Source="./Pic/Fon.jpg" Height="20" Stretch="Fill" />
<TextBlock Text="_Add" Height="16"/>
</Grid>
</Button>
Если вам нужно изменять картинки, сделайте просто Binding на сво-во Source, создайте сво-во источник (только не забудьте унаследовать от INotifPropertyChange, либо просто перенесите ранее созданное DepenecyProperty, из-за которого возникло столько работы, иначе оно не будет отслеживать изменения), и через FindAncesstor либо через ElementName (тогда нужно в XAML-разметке объявить имя окна через x:Name) привязать это сво-во.
Можно сделать ещё проще, в DepencyProperty создать обработчик при изменении, и в нём ручками привязать e.NewValue к Image.Source, только здесь нужно будет кнопку разобрать на составляющие, это если не ошибаюсь позволяет сделать FindTemplate.
2) А если таких кнопок 20? Представляешь - на что будет похоже XML-описание окна? А ведь в нем не только кнопки!