I'm trying to make it so that when the window drops below a certain size, the buttons shrink in size.
Here is the code for my Style:
<Style x:Key="AppBarButtonStyle" TargetType="AppBarButton">
<Setter Property="Width" Value="68"/>
</Style>
How can I cause all AppBarButtons to become 64 width when the window drops below 720 pixels?
We've discussed this in your another question, your AppBarSeparators are generated in the Pivot's DataTemplate.
Still you can use DataBinding with Converter to do this, and if the size of the window is changeable during the run-time, you may also need complete your data source class with INotifyPropertyChanged Interface.
For example here:
<Page.Resources>
<local:WidthConverter x:Key="cvt" />
</Page.Resources>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Pivot x:Name="docPivot" ItemsSource="{x:Bind pivotlist}" SizeChanged="docPivot_SizeChanged">
<Pivot.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Grid.Row="0">
<AppBarButton Background="Red" Icon="Accept" Label="Accept" Width="{Binding WindowWidth, Converter={StaticResource cvt}}" />
</StackPanel>
<StackPanel Orientation="Horizontal"
Grid.Row="1">
</StackPanel>
</Grid>
</DataTemplate>
</Pivot.ItemTemplate>
</Pivot>
</Grid>
The other things are the same as my answer in your last question. And the WidthConverter here is like this:
public class WidthConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
double? width = (double?)value;
if (width <= 720)
return 64;
return 68;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
This should be easy enough, not sure if you can set styles generally, or if you are restricted to named elements though, so I would create a visualstatemanager that does that first, and then explore my options further:
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="WindowStates">
<VisualState x:Name="WideState">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="720" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="Button1.Width" Value="100" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="NarrowState">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="0" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="Button1.Width" Value="68" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</Page>
Related
Issue description
I'm developing an application in which I have a ListBox where, when an element is selected, it's details are shown in an editing control.
I'm binding the SelectedItem to the control and, as I want to apply different DataTemplates, I'm trying to use VM first approach and bind directly to the control contents.
My custom TextBox style displays validation errors with a blue border (for the sake of the example). However, when using this approach, a red validation border is also shown and it's not being removed once the data is correct. This is not the expected behavior, the red border should not show at all.
I don't know if the error is in the style or in the binding.
Example and testing
I've tried different things to try to debug the error. This is not happening with the standard style nor with a DataContext approach. However, I cannot use the DataContext approach as I will need to apply different templates to different types of elements in the list.
See the pictures below.
When the data is invalid (empty) the "VM First + Custom style" option shows both the blue and the red borders:
When I write some text, the red border is not removed:
ViewModels
There are two ViewModels, one for the main window and another for each element in the list:
public class ChildViewModel : ViewModelBase
{
private string _name;
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
public override string this[string propertyName]
{
get
{
if (propertyName == nameof(Name))
{
if (string.IsNullOrEmpty(Name))
{
return "The name is mandatory.";
}
}
return base[propertyName];
}
}
}
public class ParentViewModel : ViewModelBase
{
private ChildViewModel _selectedItem;
public ObservableCollection<ChildViewModel> Collection { get; private set; }
public ChildViewModel SelectedItem
{
get => _selectedItem;
set => SetProperty(ref _selectedItem, value);
}
public ParentViewModel()
{
Collection = new ObservableCollection<ChildViewModel>();
Collection.Add(new ChildViewModel());
Collection.Add(new ChildViewModel());
}
}
public abstract class ViewModelBase : INotifyPropertyChanged, IDataErrorInfo
{
public event PropertyChangedEventHandler PropertyChanged;
public string Error => this[null];
public virtual string this[string propertyName] => string.Empty;
protected void NotifyPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected void SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
if (!EqualityComparer<T>.Default.Equals(field, value))
{
field = value;
NotifyPropertyChanged(propertyName);
}
}
}
Views
The MainWindow is just as follows, with no code behind apart from the standard.
<Window x:Class="TestApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TestApp"
Title="MainWindow" Height="300" Width="400">
<Window.DataContext>
<local:ParentViewModel />
</Window.DataContext>
<Window.Resources>
<ResourceDictionary Source="Style.xaml" />
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ListBox Grid.RowSpan="2" ItemsSource="{Binding Collection, Mode=OneWay}" SelectedItem="{Binding SelectedItem}"/>
<UserControl Grid.Column="1" Grid.Row="0" DataContext="{Binding SelectedItem}">
<StackPanel Orientation="Vertical">
<Label Content="DataContext + No style" />
<TextBox Margin="6" Text="{Binding Name, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />
<Label Content="DataContext + Custom style" />
<TextBox Margin="6" Text="{Binding Name, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource TextBoxStyle}" />
</StackPanel>
</UserControl>
<ContentControl Grid.Column="1" Grid.Row="1" Content="{Binding SelectedItem}">
<ContentControl.Resources>
<DataTemplate DataType="{x:Type local:ChildViewModel}">
<StackPanel Orientation="Vertical">
<Label Content="VM First + No style" />
<TextBox Margin="6" Text="{Binding Name, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />
<Label Content="VM First + Custom style" />
<TextBox Margin="6" Text="{Binding Name, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource TextBoxStyle}" />
</StackPanel>
</DataTemplate>
</ContentControl.Resources>
</ContentControl>
</Grid>
</Window>
Style
This is located in a ResourceDictionary named "Style.xaml". The Border and the ValidationErrorElement are separated elements in order to apply different visual states for mouse over, focused...
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="TextBoxStyle" TargetType="{x:Type TextBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TextBox}">
<Grid x:Name="RootElement">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="ValidationStates">
<VisualState x:Name="Valid" />
<VisualState x:Name="InvalidUnfocused">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="ValidationErrorElement">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="InvalidFocused">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="ValidationErrorElement">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Border x:Name="Border" BorderThickness="1" BorderBrush="Black" Opacity="1">
<Grid>
<ScrollViewer x:Name="PART_ContentHost" BorderThickness="0" IsTabStop="False" Padding="{TemplateBinding Padding}" />
</Grid>
</Border>
<Border x:Name="ValidationErrorElement" BorderBrush="Blue" BorderThickness="1" Visibility="Collapsed">
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
You are not displaying the error condition correctly.
WPF uses a ControlTemplate with an AdornedElementPlaceholder for this, which is set in the attached Validation.ErrorTemplate property.
A big example with implementation can be found here: https://stackoverflow.com/a/68748914/13349759
Here is a small snippet of XAML from another small example:
<Window.Resources>
<ControlTemplate x:Key="validationFailed">
<StackPanel Orientation="Horizontal">
<Border BorderBrush="Violet" BorderThickness="2">
<AdornedElementPlaceholder />
</Border>
<TextBlock Foreground="Red" FontSize="26" FontWeight="Bold">!</TextBlock>
</StackPanel>
</ControlTemplate>
</Window.Resources>
<Grid>
<TextBox Margin="10"
Validation.ErrorTemplate="{StaticResource validationFailed}" >
<TextBox.Text>
<Binding Path="Age">
<Binding.ValidationRules>
<DataErrorValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
I'm working on a custom items based WPF control. For the "normal" layout, think vertical StackPanel, so my xaml would look something like:
<mycontrol>
<item1 />
<item2 />
<item3 />
</mycontrol>
In this case, its simple, 3 item containers are created and all is good. The control will look like:
[item1]
[item2]
[item3]
Use case #2 is, I need to support horizontal "grouping"... ideally my xaml would look like:
<mycontrol>
<item1 />
<stackpanel orientation=horizontal>
<item2a />
<item2b />
<item2c />
</stackpanel>
<item3 />
</mycontrol>
and in this case, I would render as:
[item1]
[item2a] [item2b] [item2c]
[item3]
So, what I'm going for is 5 item containers being generated. I've already worked out a custom layout panel and that part works.
The issue is, if I use a stackpanel, I'll only get 3 item containers which makes sense, duh, but it breaks the keyboard interface of the control. I could do something hacky where I intercept all the keyboard and mouse stuff and "re-route" it in this case, but that seems hacky and difficult to get to work in a generic way.
Is there something obscure built into WPF to deal with this? The "sub items" getting generated as thier own containers?
The way I'm currently heading is to do something like:
<item1 />
<item2a isGrouped=true />
<item2b isGrouped=true />
<item2c isGrouped=true />
<item3 />
So, when I hit the first isGrouped=true, it'll start grouping until it hits a false, but I'm not crazy about that either because I'll have to make isGrouped a 3 state enum so I can have one group right below another group. Also, the hierarchy is not clear in the xaml.
Any suggestions?
I was able to get more or less the look you are going for using a HierarchicalDataTemplate within a TreeView that uses a custom TreeViewItem control template. For the control template, I simply copied the example template and made a few modifications. All of the containers are in the right place, but the keyboard navigation doesn't work on the nested items (because TreeView is not expecting that layout I guess). Here is what I came up with:
<TreeView
ItemsSource="{Binding Items}">
<TreeView.Resources>
<Color x:Key="SelectedBackgroundColor">#FFC5CBF9</Color>
<Color x:Key="SelectedUnfocusedColor">#FFDDDDDD</Color>
<Style
TargetType="{x:Type TreeViewItem}">
<Setter
Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter
Property="IsExpanded"
Value="True" />
<Setter
Property="Template">
<Setter.Value>
<!-- This template came from the example template and has just a few modifications.
Example is at: https://msdn.microsoft.com/en-us/library/ms752048.aspx -->
<ControlTemplate
TargetType="{x:Type TreeViewItem}">
<Grid>
<!-- Changed the grid configuration -->
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- The entire VisualStateGroups section is a direct copy+paste from the example template -->
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="SelectionStates">
<VisualState x:Name="Selected">
<Storyboard>
<ColorAnimationUsingKeyFrames
Storyboard.TargetName="Bd"
Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)">
<EasingColorKeyFrame
KeyTime="0"
Value="{StaticResource SelectedBackgroundColor}" />
</ColorAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Unselected" />
<VisualState x:Name="SelectedInactive">
<Storyboard>
<ColorAnimationUsingKeyFrames
Storyboard.TargetName="Bd"
Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)">
<EasingColorKeyFrame
KeyTime="0"
Value="{StaticResource SelectedUnfocusedColor}" />
</ColorAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="ExpansionStates">
<VisualState x:Name="Expanded">
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetProperty="(UIElement.Visibility)"
Storyboard.TargetName="ItemsHost">
<DiscreteObjectKeyFrame
KeyTime="0"
Value="{x:Static Visibility.Visible}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Collapsed" />
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<!-- Removed the ToggleButton -->
<!-- Tweaked the placement of items in the grid -->
<Border
x:Name="Bd"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}">
<ContentPresenter
x:Name="PART_Header"
ContentSource="Header"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"/>
</Border>
<ItemsPresenter
x:Name="ItemsHost"
Grid.Column="1" />
</Grid>
<ControlTemplate.Triggers>
<!-- Removed the IsExpanded trigger -->
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="HasHeader" Value="false" />
<Condition Property="Width" Value="Auto" />
</MultiTrigger.Conditions>
<Setter
TargetName="PART_Header"
Property="MinWidth"
Value="75" />
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="HasHeader" Value="false" />
<Condition Property="Height" Value="Auto" />
</MultiTrigger.Conditions>
<Setter
TargetName="PART_Header"
Property="MinHeight"
Value="19" />
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</TreeView.Resources>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate
DataType="{x:Type local:MyItem}"
ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Name}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
For reference, my ItemsSource property is bound to a collection containing items that look like this:
internal class MyItem
{
public string Name { get; private set; }
public List<MyItem> Children { get; private set; }
public MyItem(string name = null)
{
Name = name;
Children = new List<MyItem>();
}
}
This is done without creating a custom control, but hopefully it gives you an idea of what you can do. The combination of a HeirarchicalDataTemplate and a specialized container type (in my case TreeViewItem with a custom control template) is key to the way I did it.
Here is some data that I tested with and the result:
public IEnumerable<MyItem> Items { get; private set; }
...
var items = new MyItem[]
{
new MyItem("[First]"),
new MyItem(),
new MyItem("[Third]")
};
items[1].Children.Add(new MyItem("[Second_0]"));
items[1].Children.Add(new MyItem("[Second_1]"));
items[1].Children.Add(new MyItem("[Second_2]"));
Items = items;
I am sure the visualization could be improved. I just threw this together in under 10 minutes.
I am using a Telerik RadTimeline control in my project, I have a template for the TimelineItemTemplate:
<telerik:RadTimeline x:Name="myTimeline"
PeriodStart="{Binding PeriodStart}"
PeriodEnd="{Binding PeriodEnd}"
VisiblePeriodStart="{Binding VisiblePeriodStart, Mode=TwoWay}"
VisiblePeriodEnd="{Binding VisiblePeriodEnd, Mode=TwoWay}"
StartPath="StartDate"
DurationPath="Duration"
GroupPath="Title"
GroupExpandMode="Multiple"
ItemsSource="{Binding myData}"
TimelineItemTemplate="{StaticResource myTemplate}"
ScrollMode="ScrollOnly"
IsSelectionEnabled="True" SelectionMode="Extended"
DataContext="{Binding ElementName=vm}">
</telerik:RadTimeline>
My template is the next:
<DataTemplate x:Key="myTemplate">
<controls:CustomTimelineControl Style="{StaticResource myStyle}"/>
</DataTemplate>
And my style:
<Style x:Key="myStyle" TargetType="controls:CustomTimelineControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="controls:CustomTimelineControl">
<Border Height="{Binding Source={StaticResource odpSettings}, UpdateSourceTrigger=, Converter={StaticResource visibleToHeightConverter3}, ConverterParameter=Largos}" Margin="{Binding Source={StaticResource odpSettings},Path=ItemBorderMargin,Mode=OneWay}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="MouseOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="largosHeatItem" Storyboard.TargetProperty="Background" Duration="0.00:00:00.05">
<DiscreteObjectKeyFrame KeyTime="0.00:00:00.0" Value="{StaticResource HeatItem_Background_MouseOver}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="largosHeatItem" Storyboard.TargetProperty="Background" Duration="0.00:00:00.05">
<DiscreteObjectKeyFrame KeyTime="0.00:00:00.0" Value="{StaticResource HeatItem_Background_MouseOver}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Border x:Name="largosHeatItem" Height="{Binding Source={StaticResource odpSettings}, UpdateSourceTrigger=PropertyChanged, Converter={StaticResource visibleToHeightConverter2}, ConverterParameter=Largos}"
Margin="0, 0, 0, 0" Background="{Binding DataItem.Status, Converter={StaticResource typeToColorConverter}}">
<TextBlock Text="{Binding DataItem.HeatId}"
FontFamily="Segoe UI"
Foreground="{StaticResource HeatItem_Foreground}"
FontSize="{Binding Source={StaticResource odpSettings}, UpdateSourceTrigger=PropertyChanged, Converter={StaticResource visibleToFontSizeConverter}}"
Height="{Binding Source={StaticResource odpSettings}, UpdateSourceTrigger=PropertyChanged, Converter={StaticResource visibleToHeightConverter1}, ConverterParameter=Largos}"
TextAlignment="Center"/>
</Border>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
I have converters for the borders height, textblock size and height, as value I am sending an object that contains several properties in order to set the correct value, for example:
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
AppSettings objSettings = value as AppSettings;
double val = 0;
if (parameter != null && parameter is string)
{
switch((string)parameter)
{
case "Planos":
if(objSettings.BothGantt)
val= objSettings.PlanosItemHeight1;
else
val= objSettings.PlanosItemHeightMax1;
break;
case "Largos":
if(objSettings.BothGantt)
val= objSettings.LargosItemHeight1;
else
val= objSettings.LargosItemHeightMax1;
break;
}
}
return val;
}
opdSettings is an ObjectDataProvider:
<ObjectDataProvider x:Key="odpSettings" ObjectType="{x:Type src:AppSettings}"/>
When the application is loaded, the control is shown correctly, but then when the appication is running I need to change some properties of odpSettings that affects the Timeline layout but it doesn't show the new values.
Is there a way to "refresh" the converters binding so the control shows the changes? I tried to use UpdateSourceTrigger=PropertyChanged but it didn't work.
Or is there another way to accomplish my goal?
Thanks in advance.
Alberto
Well it's not really very dynamic, at least it won't change at runtime.
The idea is I have buttons and each one has a unique image ( icon 32x32 ). The buttons all share a style where I mess with the ControlTemplate. So each image also has 2 colors one normal and another when I mouse over.
I noticed that when I declare the source path for the images is that they are almost all the same so I though DRY ( Don't Repeat Yourself ). What if I could use the button Name or some other property as part of the source path ( i.e. the name of the image file ). That would be good programming.
Problem is I'm new to XAML, WPF and perhaps programming all together so I'm not sure how to do that. I guess this would need code behind or a converter of some sort ( guess I'll read about converters a bit more ). Here is a bit of code ( this doesn't work but it gives you the general idea ( hopefully )):
<Style x:Key="ButtonPanelBigButton" TargetType="{x:Type Button}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Name="ButtonBorder"
Height="78"
MaxWidth="70"
MinWidth="50"
BorderThickness="0.5"
BorderBrush="Transparent"
CornerRadius="8" >
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="2*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Here I wan't to put in the Name property of the button because there is a picture there to match -->
<Image x:Name="ButtonIcon" Source="..\Images\Icons\32x32\Blue\{Binding Name}.png"
Margin="4"
Height="32"
Width="32"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
<TextBlock Grid.Row="1"
Padding="5,2,5,2"
TextWrapping="Wrap"
Style="{StaticResource MenuText}"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<ContentPresenter ContentSource="Content" />
</TextBlock>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True" >
<Setter TargetName="ButtonIcon" Property="Source" Value="..\Images\Icons\32x32\Green\user.png" /> <!-- Same Here -->
<Setter TargetName="ButtonBorder" Property="BorderBrush" Value="{StaticResource SecondColorBrush}" />
<Setter TargetName="ButtonBorder" Property="Background">
<Setter.Value>
<LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1" Opacity="0.5">
<GradientStop Color="{StaticResource MainColor}" Offset="1" />
<GradientStop Color="{StaticResource SecondColor}" Offset="0.5" />
<GradientStop Color="{StaticResource MainColor}" Offset="0" />
</LinearGradientBrush>
</Setter.Value>
</Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Hopefully you get where I'm going with this and someone might be able to help me so my code is nice and DRY ( now I'M repeating myself!!! ).
You're right: The easy way to solve this is to use a converter.
The Source property takes an ImageSource, so you'll need to load the bitmap yourself in your converter.
The converter is used like this:
<Image Source="{Binding Name,
RelativeSource={RelativeSource TemplatedParent},
Converter={x:Static local:ImageSourceLoader.Instance},
ConverterParameter=..\Images\Icons\32x32\Blue\{0}.png}" />
And implemented like this:
public class ImageSourceLoader : IValueConverter
{
public static ImageSourceLoader Instance = new ImageSourceLoader();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var path = string.Format((string)parameter, value.ToString());
return BitmapFrame.Create(new Uri(path, UriKind.RelativeOrAbsolute));
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Note that this simple solution can't handle relative Uris because the BaseUri property of the Image is unavailable to the converter. If you want to use relative Uris, you can do this by binding an attached property and using StringFormat:
<Image local:ImageHelper.SourcePath="{Binding Name,
RelativeSource={RelativeSource TemplatedParent},
StringFormat=..\Images\Icons\32x32\Blue\{0}.png}" />
And the attached property's PropertyChangedCallback handles loads the image after combining the BaseUri with the formatted Uri string:
public class ImageHelper : DependencyObject
{
public static string GetSourcePath(DependencyObject obj) { return (string)obj.GetValue(SourcePathProperty); }
public static void SetSourcePath(DependencyObject obj, string value) { obj.SetValue(SourcePathProperty, value); }
public static readonly DependencyProperty SourcePathProperty = DependencyProperty.RegisterAttached("SourcePath", typeof(string), typeof(ImageHelper), new PropertyMetadata
{
PropertyChangedCallback = (obj, e) =>
{
((Image)obj).Source =
BitmapFrame.Create(new Uri(((IUriContext)obj).BaseUri, (string)e.NewValue));
}
});
}
You can use the Tag Property and set the full path of the image in Tag and then use it.
the following my code to be more clear
*<Style TargetType="{x:Type Button}">
<Setter Property="Background" Value="#373737" />
<Setter Property="Foreground" Value="White" />
<Setter Property="FontSize" Value="12" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border CornerRadius="7" Background="{TemplateBinding Background}" FlowDirection="RightToLeft">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50"/>
<ColumnDefinition Width="2"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Image x:Name="imgDistance" Source="{Binding Tag,RelativeSource={RelativeSource TemplatedParent}}"
Width="35" Height="35" HorizontalAlignment="Left" Margin="42,7,0,8" Grid.Column="0"/>*
and then I used it in the button as following
<Button x:Name="btnDistance" Height="50" VerticalAlignment="Top" Margin="0,4,0,0" Curs
or="Hand" Click="btnDistance_Click" Tag="/Images/distance.png">
I've got two controls, a TextBlock and a PopUp. When the user clicks (MouseDown) on the textblock, I want to display the popup. I would think that I could do this with an EventTrigger on the Popup, but I can't use setters in an EventTrigger, I can only start storyboards. I want to do this strictly in XAML, because the two controls are in a template and I don't know how I'd find the popup in code.
This is what conceptually I want to do, but can't because you can't put a setter in an EventTrigger (like you can with a DataTrigger):
<TextBlock x:Name="CCD">Some text</TextBlock>
<Popup>
<Popup.Style>
<Style>
<Style.Triggers>
<EventTrigger SourceName="CCD" RoutedEvent="MouseDown">
<Setter Property="Popup.IsOpen" Value="True" />
</EventTrigger>
</Style.Triggers>
</Style>
</Popup.Style>
...
What is the best way to show a popup strictly in XAML when an event happens on a different control?
I did something simple, but it works.
I used a typical ToggleButton, which I restyled as a textblock by changing its control template. Then I just bound the IsChecked property on the ToggleButton to the IsOpen property on the popup. Popup has some properties like StaysOpen that let you modify the closing behavior.
The following works in XamlPad.
<StackPanel>
<ToggleButton Name="button">
<ToggleButton.Template>
<ControlTemplate TargetType="ToggleButton">
<TextBlock>Click Me Here!!</TextBlock>
</ControlTemplate>
</ToggleButton.Template>
</ToggleButton>
<Popup IsOpen="{Binding IsChecked, ElementName=button}" StaysOpen="False">
<Border Background="LightYellow">
<TextBlock>I'm the popup</TextBlock>
</Border>
</Popup>
</StackPanel>
The following approach is the same as Helge Klein's, except that the popup closes automatically when you click anywhere outside the Popup (including the ToggleButton itself):
<ToggleButton x:Name="Btn" IsHitTestVisible="{Binding ElementName=Popup, Path=IsOpen, Mode=OneWay, Converter={local:BoolInverter}}">
<TextBlock Text="Click here for popup!"/>
</ToggleButton>
<Popup IsOpen="{Binding IsChecked, ElementName=Btn}" x:Name="Popup" StaysOpen="False">
<Border BorderBrush="Black" BorderThickness="1" Background="LightYellow">
<CheckBox Content="This is a popup"/>
</Border>
</Popup>
"BoolInverter" is used in the IsHitTestVisible binding so that when you click the ToggleButton again, the popup closes:
public class BoolInverter : MarkupExtension, IValueConverter
{
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool)
return !(bool)value;
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return Convert(value, targetType, parameter, culture);
}
}
...which shows the handy technique of combining IValueConverter and MarkupExtension in one.
I did discover one problem with this technique: WPF is buggy when two popups are on the screen at the same time. Specifically, if your toggle button is on the "overflow popup" in a toolbar, then there will be two popups open after you click it. You may then find that the second popup (your popup) will stay open when you click anywhere else on your window. At that point, closing the popup is difficult. The user cannot click the ToggleButton again to close the popup because IsHitTestVisible is false because the popup is open! In my app I had to use a few hacks to mitigate this problem, such as the following test on the main window, which says (in the voice of Louis Black) "if the popup is open and the user clicks somewhere outside the popup, close the friggin' popup.":
PreviewMouseDown += (s, e) =>
{
// Workaround for popup not closing automatically when
// two popups are on-screen at once.
if (Popup.IsOpen)
{
Point p = e.GetPosition(Popup.Child);
if (!IsInRange(p.X, 0, ((FrameworkElement)Popup.Child).ActualWidth) ||
!IsInRange(p.Y, 0, ((FrameworkElement)Popup.Child).ActualHeight))
Popup.IsOpen = false;
}
};
// Elsewhere...
public static bool IsInRange(int num, int lo, int hi) =>
num >= lo && num <= hi;
The following uses EventTrigger to show the Popup. This means we don't need a ToggleButton for state binding.
In this example the Click event of a Button is used. You can adapt it to use another element/event combination.
<Button x:Name="OpenPopup">Popup
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<BooleanAnimationUsingKeyFrames
Storyboard.TargetName="ContextPopup"
Storyboard.TargetProperty="IsOpen">
<DiscreteBooleanKeyFrame KeyTime="0:0:0" Value="True" />
</BooleanAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Button.Triggers>
</Button>
<Popup x:Name="ContextPopup"
PlacementTarget="{Binding ElementName=OpenPopup}"
StaysOpen="False">
<Label>Popupcontent...</Label>
</Popup>
Please note that the Popup is referencing the Button by name and vice versa. So x:Name="..." is required on both, the Popup and the Button.
It can actually be further simplified by replacing the Storyboard stuff with a custom SetProperty EventTrigger Action described in this SO Answer
I had some issues with the MouseDown part of this, but here is some code that might get your started.
<Window x:Class="WpfApplication1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Grid>
<Control VerticalAlignment="Top">
<Control.Template>
<ControlTemplate>
<StackPanel>
<TextBox x:Name="MyText"></TextBox>
<Popup x:Name="Popup" PopupAnimation="Fade" VerticalAlignment="Top">
<Border Background="Red">
<TextBlock>Test Popup Content</TextBlock>
</Border>
</Popup>
</StackPanel>
<ControlTemplate.Triggers>
<EventTrigger RoutedEvent="UIElement.MouseEnter" SourceName="MyText">
<BeginStoryboard>
<Storyboard>
<BooleanAnimationUsingKeyFrames Storyboard.TargetName="Popup" Storyboard.TargetProperty="(Popup.IsOpen)">
<DiscreteBooleanKeyFrame KeyTime="00:00:00" Value="True"/>
</BooleanAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="UIElement.MouseLeave" SourceName="MyText">
<BeginStoryboard>
<Storyboard>
<BooleanAnimationUsingKeyFrames Storyboard.TargetName="Popup" Storyboard.TargetProperty="(Popup.IsOpen)">
<DiscreteBooleanKeyFrame KeyTime="00:00:00" Value="False"/>
</BooleanAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Control.Template>
</Control>
</Grid>
</Window>
another way to do it:
<Border x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="true">
<StackPanel>
<Image Source="{Binding ProductImage,RelativeSource={RelativeSource TemplatedParent}}" Stretch="Fill" Width="65" Height="85"/>
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
<Button x:Name="myButton" Width="40" Height="10">
<Popup Width="100" Height="70" IsOpen="{Binding ElementName=myButton,Path=IsMouseOver, Mode=OneWay}">
<StackPanel Background="Yellow">
<ItemsControl ItemsSource="{Binding Produkt.SubProducts}"/>
</StackPanel>
</Popup>
</Button>
</StackPanel>
</Border>