Icon badge overlay for notifications in silverlight/xaml - silverlight

I've got a ribbon bar in my silverlight app and on one of the icons I would like for there to be a badge icon showing the number of items in the view that the icon activates.
Picture the Mail icon in OS X showing the number of unread messages or the notifications counter on an IOS app icon.
I don't know much about xaml styles, but it seems to me I could duplicate the default style for the ribbon bar button, then add to it with some sort of red circle, and a white text that took in its value from a new property on the ribbon bar button somehow so I would be able to bind to it.
Does anyone have an example of something like this I can start from?
Thanks Shawn for the answer. This is what I ended up doing:
In the xaml:
<telerikRibbonBar:RadRibbonRadioButton
Text="Expired Active Call Factors"
Size="Large"
LargeImage="/CallFactorDatabase.UI;component/Images/Ribbon/Large/ExpiredActiveView.png"
Command="{Binding ActivateViewCommand}"
CommandParameter="ExpiredActiveView">
<Grid>
<Grid.Resources>
<converters:BooleanToVisibilityConverter x:Key="visibleWhenTrueConverter" VisibilityWhenTrue="Visible" VisibilityWhenFalse="Collapsed" />
</Grid.Resources>
<Grid Width="27" Height="27" Visibility="{Binding ExpiredActiveCallFactors, Converter={StaticResource visibleWhenTrueConverter}}" Margin="50,-40,0,0">
<Ellipse Fill="Black" Width="27" Height="27"/>
<Ellipse Width="25" Height="25" VerticalAlignment="Center" HorizontalAlignment="Center">
<Ellipse.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="Coral" Offset="0.0" />
<GradientStop Color="Red" Offset="1.0" />
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Viewbox Width="25" Height="25" VerticalAlignment="Center" HorizontalAlignment="Center" >
<TextBlock Text="{Binding ExpiredActiveCallFactorsCount}" Foreground="White"/>
</Viewbox>
</Grid>
</Grid>
</telerikRibbonBar:RadRibbonRadioButton>
How it looks:
No luck getting it in front of the ribbon button but oh well.

This can be accomplished with a few bindings and an optional value converter. This samples assumes you are binding to a model that has an Items property and that that property is of type ObservableCollection so that the Count property of the collection will fire property changed when items are added/removed.
<Grid>
<Grid.Resources>
<local:CountToVisbilityConverter x:Key="CountToVis"/>
</Grid.Resources>
....
<Grid Width="25" Height="25" Visibility="{Binding Items.Count, Converter=CountToVis}">
<Ellipse Fill="Red" Width="25" Height="25"/>
<ViewBox Width="25" Height="25">
<TextBlock Text="{Binding Itmes.Count}" Foreground="White"/>
</Viewbox>
</Grid>
</Grid>
And the value converter:
public class CountToVisibilityConverter : IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if(value == null) return Visibility.Collapsed;
int count = System.Convert.ToInt32(value);
return count == 0 ? Visibility.Collapsed : Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
The reason I saw optional" converter is because you can also use the Interaction DataTriggers like such
<Grid x:Name="UnreadNotification" Width="25" Height="25">
<Ellipse Fill="Red" Width="25" Height="25"/>
<ViewBox Width="25" Height="25">
<TextBlock Text="{Binding Itmes.Count}" Foreground="White"/>
</Viewbox>
</Grid>
<i:Interaction.Triggers>
<ei:DataTrigger Binding="{Binding Items.Count, Comparison="Equal"
Value="0">
<ei:ChangePropertyAction PropertyName="IsEnabled"
Value="True"
TargetName="UnreadNotification" />
</ei:DataTrigger>
</i:Interaction.Triggers>

Here's my solution for this. By default, the badge will show in the upper right corner. You can change this by setting the "BadgeMarginOffset" property. I've attached a couple of images to show how it appears. One, which shows the badge wrapping a Telerik RadRibbonButton. You can also change the background and foreground colors of the badge (BadgeBackground, BadgeForeground). The defaults are shown below.
The UserControl's XAML
<UserControl x:Class="Foundation.Common.Controls.Wpf.Badge"
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:converters="clr-namespace:Foundation.Common.Controls.Wpf.Converters"
Background="Transparent" x:Name="UserControl"
mc:Ignorable="d" >
<UserControl.Resources>
<converters:GreaterThanZeroBooleanConverter x:Key="GreaterThanZeroBooleanConverter" />
<converters:GreaterThanZeroVisibilityConverter x:Key="GreaterThanZeroVisibilityConverter"/>
</UserControl.Resources>
<UserControl.Template>
<ControlTemplate>
<Grid HorizontalAlignment="Stretch" >
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Border CornerRadius="10"
UseLayoutRounding="True"
x:Name="BadgeBorder"
Grid.ZIndex="99"
VerticalAlignment="Top"
HorizontalAlignment="Right"
Visibility="{Binding ElementName=UserControl, Path=Count, Mode=TwoWay, Converter={StaticResource GreaterThanZeroVisibilityConverter}}"
Grid.Row="0"
Margin="{Binding ElementName=UserControl, Path=BadgeMarginOffset}"
Height="22"
Padding="6.7,2,7,3"
Background="{Binding ElementName=UserControl, Path=BadgeBackground}">
<Border.Style>
<Style TargetType="Border">
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=UserControl, Path=Count, Mode=TwoWay, Converter={StaticResource GreaterThanZeroBooleanConverter}}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation From="0.0"
To="1.0"
Duration="0:0:0.7"
Storyboard.TargetProperty="Opacity"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
<DataTrigger Binding="{Binding ElementName=UserControl, Path=ShowDropShadow}" Value="True">
<Setter Property="Effect">
<Setter.Value>
<DropShadowEffect BlurRadius="6" ShadowDepth="4" Color="#949494"/>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock Text="{Binding ElementName=UserControl, Path=Count}"
Foreground="{Binding ElementName=UserControl, Path=BadgeForeground}"
FontWeight="Bold"
FontSize="12">
</TextBlock>
</Border>
<ContentPresenter Grid.Row="0"
Grid.RowSpan="2"
DataContext="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=DataContext}"
Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Content}" />
</Grid>
</ControlTemplate>
</UserControl.Template>
The UserControl's code behind
public partial class Badge : UserControl
{
#region Dependency Properties
public static readonly DependencyProperty CountProperty =
DependencyProperty.Register("Count", typeof(int), typeof(Badge));
public static readonly DependencyProperty ShowDropShadowProperty =
DependencyProperty.Register("ShowDropShadow", typeof(bool), typeof(Badge), new PropertyMetadata(true));
public static readonly DependencyProperty BadgeMarginOffsetProperty =
DependencyProperty.Register("BadgeMarginOffset", typeof(Thickness), typeof(Badge));
public static readonly DependencyProperty BadgeBackgroundProperty =
DependencyProperty.Register("BadgeBackground", typeof(Brush), typeof(Badge), new PropertyMetadata(Brushes.Red));
public static readonly DependencyProperty BadgeForegroundProperty =
DependencyProperty.Register("BadgeForeground", typeof(Brush), typeof(Badge), new PropertyMetadata(Brushes.White));
#endregion Dependency Properties
#region Constructor
/// <summary>
/// Initializes a new instance of the <see cref="Badge"/> class.
/// </summary>
public Badge()
{
this.InitializeComponent();
}
#endregion Constructor
#region Properties
/// <summary>
/// Gets or sets a value indicating whether [show drop shadow].
/// </summary>
/// <value>
/// <c>true</c> if [show drop shadow]; otherwise, <c>false</c>.
/// </value>
public bool ShowDropShadow
{
get => (bool)this.GetValue(ShowDropShadowProperty);
set => this.SetValue(ShowDropShadowProperty, value);
}
/// <summary>
/// Gets or sets the badge margin offset.
/// </summary>
/// <value>
/// The badge margin offset.
/// </value>
public Thickness BadgeMarginOffset
{
get => (Thickness)this.GetValue(BadgeMarginOffsetProperty);
set => this.SetValue(BadgeMarginOffsetProperty, value);
}
/// <summary>
/// Gets or sets the badge background.
/// </summary>
/// <value>
/// The badge background.
/// </value>
public Brush BadgeBackground
{
get => (Brush)this.GetValue(BadgeBackgroundProperty);
set => this.SetValue(BadgeBackgroundProperty, value);
}
/// <summary>
/// Gets or sets the badge foreground.
/// </summary>
/// <value>
/// The badge foreground.
/// </value>
public Brush BadgeForeground
{
get => (Brush)this.GetValue(BadgeForegroundProperty);
set => this.SetValue(BadgeBackgroundProperty, value);
}
/// <summary>
/// Gets or sets the count.
/// </summary>
/// <value>
/// The count.
/// </value>
public int Count
{
get => (int)this.GetValue(CountProperty);
set => this.SetValue(CountProperty, value);
}
#endregion Properties
}
Sample code for the top two images
<wpf:Badge Count="108" Margin="20" HorizontalAlignment="Left" BadgeMarginOffset="0,-5,-5,0">
<Button Height="100" Width="100">
<Button.Content>
<Image Source="Resources/about.png" />
</Button.Content>
</Button>
</wpf:Badge>

Related

<EventTrigger RoutedEvent="Binding.TargetUpdated"> not firing with ContentTemplateSelector

I'm trying to animate the Background Color of a Border in a DataTemplate for a DataObject when a Child Property of the DataObject changes.
The DataObject is a Class called Test with two Properties, Number and Text.
I have an ObservableCollection of DataObjects called Numbers.
In a Task I update the Number Property at a regular Interval.
<Window
x:Class="WpfAnimationTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfAnimationTest"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="MainWindow"
Width="800"
Height="450"
DataContext="{Binding Main, Source={StaticResource Locator}}"
mc:Ignorable="d">
<Grid>
<Grid.Resources>
<DataTemplate x:Key="NumberTemplate">
<TextBlock Text="{Binding NotifyOnTargetUpdated=True}" />
</DataTemplate>
<local:ValueTemplateSelector x:Key="TemplateSelector">
<local:ValueTemplateSelector.NumberTemplate>
<DataTemplate>
<ContentControl Content="{Binding NotifyOnTargetUpdated=True}" ContentTemplate="{StaticResource NumberTemplate}" />
</DataTemplate>
</local:ValueTemplateSelector.NumberTemplate>
</local:ValueTemplateSelector>
<DataTemplate DataType="{x:Type local:Test}">
<Border x:Name="UpdateBorder" Background="Aqua">
<StackPanel Orientation="Horizontal">
<TextBlock
Width="50"
Margin="10"
Text="{Binding Text}" />
<ContentControl
Width="50"
Margin="10"
Content="{Binding Number}"
ContentTemplateSelector="{StaticResource TemplateSelector}" />
<!--
ContentTemplate="{StaticResource NumberTemplate}"
-->
</StackPanel>
</Border>
<DataTemplate.Triggers>
<EventTrigger RoutedEvent="Binding.TargetUpdated">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard AutoReverse="True">
<ColorAnimation
FillBehavior="Stop"
Storyboard.TargetName="UpdateBorder"
Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
To="#C5AFFFAA"
Duration="00:00:0.5" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</Grid.Resources>
<ListBox ItemsSource="{Binding Numbers}" />
</Grid>
</Window>
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
namespace WpfAnimationTest
{
public class Locator
{
public Locator()
{
Main = new Main();
}
public Main Main { get; set; }
}
public class Main
{
public ObservableCollection<Test> Numbers { get; set; } = new ObservableCollection<Test>();
public Main()
{
var rnd = new Random(42);
for (int i = 0; i < 10; i++)
{
Numbers.Add(new Test(){Number = i, Text = $"#: {i}"});
}
Task.Run(() =>
{
while (true)
{
try
{
Application.Current?.Dispatcher.Invoke(() =>
{
Numbers[rnd.Next(9)] = new Test
{
Number = rnd.Next(30),
Text = $"# {rnd.Next(30) + 30}"
};
});
Thread.Sleep(1000);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
});
}
}
public class Test
{
public int Number { get; set; }
public string Text { get; set; }
}
public class ValueTemplateSelector : DataTemplateSelector
{
/// <summary>
///
/// </summary>
/// <param name="item"></param>
/// <param name="container"></param>
/// <returns></returns>
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
return !(item is Test value)
? null
: NumberTemplate;
}
/// <summary>
///
/// </summary>
public DataTemplate NumberTemplate { get; set; }
/// <summary>
///
/// </summary>
public DataTemplate DefaultTemplate { get; set; }
}
}
When using a ContentTemplate for the Number Property in the ContentControl the animation is working.
But when I use a ContentTemplateSelector the Animation is not triggered anymore.
What am I missing here?
Your data bindings are wrong, resulting in unhandled or unexpected data types.
Currently, your ContentControl.Content property (inside the DataTemplate for the type Test) binds to the Test.Number property of type int. Therefore, the object that is passed to the DataTemplateSelector is of type int and not Test. The condition in the DataTemplateSelector.SelectTemplate method will return false (as int is not Test is true) and make the DataTemplateSelector.SelectTemplate return null - no template. The XAML engine will call ToString on the int instance and is therefore able to display the value correctly (despite the lack of a DataTemplate).
Solution
You must fix the binding on the ContentControl.Content property to bind to the Test instance instead of binding to the Test.Number property:
<DataTemplate DataType="{x:Type local:Test}">
<Border x:Name="UpdateBorder"
Background="Aqua">
<StackPanel Orientation="Horizontal">
<TextBlock Width="50"
Margin="10"
Text="{Binding Text}" />
<ContentControl Width="50"
Margin="10"
Content="{Binding}"
ContentTemplateSelector="{StaticResource TemplateSelector}" />
</StackPanel>
</Border>
...
</DataTemplate>
Now that that the DataTemplateSelector can work properly, you must also adjust the NumberTemplate, so that it can display the Test.Number property properly. Since we have modified the binding source for the ContentControl.Content property, the new data type is now Test (instead of int):
<DataTemplate x:Key="NumberTemplate"
DataType="{x:Type Test}">
<TextBlock Text="{Binding Number, NotifyOnTargetUpdated=True}" />
</DataTemplate>
Remarks
There is a lot of redundant code here. You will achieve the same results by dropping all the templates and the DataTemplateSelector by defining the ItemTemplate for the Test items properly.
The following drastically simplified version should also work:
<Grid>
<Grid.Resources>
<DataTemplate DataType="{x:Type local:Test}">
<Border x:Name="UpdateBorder"
Background="Aqua">
<StackPanel Orientation="Horizontal">
<TextBlock Width="50"
Margin="10"
Text="{Binding Text}" />
<TextBlock Width="50"
Margin="10"
Text="{Binding Number, NotifyOnTargetUpdated=True}" />
</StackPanel>
</Border>
<DataTemplate.Triggers>
<EventTrigger RoutedEvent="Binding.TargetUpdated">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard AutoReverse="True">
<ColorAnimation FillBehavior="Stop"
Storyboard.TargetName="UpdateBorder"
Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
To="#C5AFFFAA"
Duration="00:00:0.5" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</Grid.Resources>
<ListBox ItemsSource="{Binding Numbers}" />
</Grid>

Custom User Control not binding data [duplicate]

This question already has answers here:
Issue with DependencyProperty binding
(3 answers)
Datacontext conflicts
(1 answer)
How to correctly bind to a dependency property of a usercontrol in a MVVM framework
(4 answers)
Closed 5 years ago.
I've created a user control that have a label and textbox.
i added two DependencyProperties (Text and Label) and bind them to textbox.text and label.content.
however, i'm not able to see the text of textbox.
in the main window, when i'm not binding to any element the label is shown but if i binding the element is not shown. the textbox not showing either way.
here's the xaml:
<UserControl x:Class="TestNewLabeltextbox.UserControl1"
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">
<StackPanel Orientation="Horizontal" Background="White" FlowDirection="RightToLeft">
<Label x:Name="lbl" Content="{Binding Label, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Width="100" HorizontalAlignment="Left" Background="blue">
<Label.Style>
<Style TargetType="Label">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Label">
<StackPanel Orientation="Horizontal">
<Border Background="Blue" Width="200" BorderThickness="0,0,0,0">
<StackPanel Orientation="Horizontal">
<Viewbox StretchDirection="DownOnly" Stretch="Uniform">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" RecognizesAccessKey="True" TextBlock.FontSize="14" TextBlock.Foreground="#FFFFFF" Margin="5">
<ContentPresenter.Effect>
<DropShadowEffect BlurRadius="0.0"
Color="#032A6B"
Direction="90"
Opacity="1"
ShadowDepth="1" />
</ContentPresenter.Effect>
</ContentPresenter>
</Viewbox>
</StackPanel>
</Border>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Label.Style>
</Label>
<TextBox x:Name="txt" Text="{Binding Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Width="120" HorizontalAlignment="Right">
<TextBox.Style>
<Style TargetType="TextBox">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TextBox">
<Border CornerRadius="0,0,0,50" BorderBrush="Black" Background="White" BorderThickness="0">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" RecognizesAccessKey="True" TextBlock.FontSize="14" TextBlock.Foreground="#FFFFFF" Margin="5">
<ContentPresenter.Effect>
<DropShadowEffect BlurRadius="0.0"
Color="#032A6B"
Direction="90"
Opacity="1"
ShadowDepth="1" />
</ContentPresenter.Effect>
</ContentPresenter>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</TextBox.Style>
</TextBox>
</StackPanel>
Here'sUserControl1.cs:
public partial class UserControl1 : UserControl
{
public UserControl1()
{
InitializeComponent();
this.DataContext = this;
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
}
public static readonly DependencyProperty LabelProperty = DependencyProperty.Register("Label", typeof(string), typeof(UserControl1), new PropertyMetadata(null));
public string Label
{
get { return (string)this.GetValue(LabelProperty); }
set { this.SetValue(LabelProperty, value); }
}
public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(UserControl1), new PropertyMetadata(null));
public string Text
{
get { return (string)this.GetValue(TextProperty); }
set { this.SetValue(TextProperty, value); }
}
}
here's the window xaml + cs:
<Window x:Class="TestNewLabeltextbox.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:TestNewLabeltextbox"
Title="MainWindow" Height="350" Width="525">
<Grid>
<StackPanel Orientation="Vertical" Height="150">
<controls:UserControl1 Text="hello" Height="50" Label="{Binding Hello, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<controls:UserControl1 Text="hello" Height="50" Label="world" />
<Label BorderBrush="Black" BorderThickness="2" Width="100" Height="50" Content="{Binding Hello, Mode=TwoWay}"/>
</StackPanel>
</Grid>
public partial class MainWindow : Window
{
ViewModel vm = new ViewModel();
public MainWindow()
{
InitializeComponent();
vm.Hello = "555";
this.DataContext = vm;
}
}
viewmodel.cs
public class ViewModel : INotifyPropertyChanged
{
private string h = "Hello";
public string Hello
{
get
{
return h;
}
set
{
h = value;
NotifyPropertyChanged("Hello");
}
}
#region "PropertyChanged Event"
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
Default Source of binding is DataContext. But your Label and Text dependency properties defined in the control rather than in view-model. Change binding of Label to
{Binding Path=Label, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}}}
and binding of TextBox to
{Binding Path=Text, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}
Please read about Mode and UpdateSourceTrigger properties of Binding. It seems that you don't know how they work. Mode=TwoWay, UpdateSourceTrigger=PropertyChanged doesn't make any sense for Content property.

UserControl multiple datatemplate + templateselector

I need to show data inside a usercontrol in different ways depending on a flag.
To achieve this i tried the following, but using this control in the main view shows nothing.
<UserControl DataContext="**self**">
<UserControl.Resources>
<DataTemplate x:Key="mouseInputTemplate">
<TextBlock HorizontalAlignment="Center"><Run Text="{Binding Text}" /></TextBlock>
</DataTemplate>
<DataTemplate x:Key="touchInputTemplate">
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Image Source="{Binding ImageUri}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Width="{Binding ImageWidth}" Height="{Binding ImageHeight}" />
<TextBlock HorizontalAlignment="Center"><Run Text="{Binding Text}" /></TextBlock>
</StackPanel>
</DataTemplate>
<local:InputModeDataTemplateSelector x:Key="inputModeTemplateSelector"
MouseInputModeTemplate="{StaticResource mouseInputTemplate}"
TouchInputModeTemplate="{StaticResource touchInputTemplate}" />
</UserControl.Resources>
<ContentControl HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" VerticalAlignment="Stretch"
VerticalContentAlignment="Stretch" ContentTemplateSelector="{StaticResource inputModeTemplateSelector}" />
</UserControl>
What am i doing wrong?
Is there a better way to achieve that?
Thank to EdPlunkett and more research i found out it is better to
use a ContentPresenter here and instead of binding on DataContext=this bind like this (as alsways suggested when writing a UserControl)
DataContext="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type yourType}}}"
Code:
<UserControl.Resources>
<DataTemplate x:Key="touchInputTemplate">
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Image Source="{Binding ImageUri}" Width="64" Height="64" />
<TextBlock HorizontalAlignment="Center" Text="{Binding Text}" />
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="mouseInputTemplate">
<TextBlock HorizontalAlignment="Center" Text="{Binding Text}" />
</DataTemplate>
<local:InputModeDataTemplateSelector x:Key="inputModeTemplateSelector"
MouseInputModeTemplate="{StaticResource mouseInputTemplate}"
TouchInputModeTemplate="{StaticResource touchInputTemplate}" />
</UserControl.Resources>
<Grid DataContext="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type yourType}}}">
<ContentPresenter Content="{Binding}" ContentTemplateSelector="{StaticResource inputModeTemplateSelector}">
</Grid>
Your ContentPresenter idea is the correct way to do it with a DataTemplateSelector, and I should have thought of it myself.
But here's yet another way to do it, which unlike my first answer, actually solves all the problems you're having:
XAML (in practice the Style would probably be defined in a separate ResourceDictionary):
<Window
x:Class="TestApplication.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TestApplication"
xmlns:diag="clr-namespace:System.Diagnostics;assembly=WindowsBase"
Title="MainWindow" Height="350" Width="525"
>
<Window.Resources>
<Style TargetType="local:TestControl">
<Setter Property="Background" Value="Gainsboro" />
<Style.Triggers>
<!-- The 0 value for the InputMode enum is Mouse, so this will be the default. -->
<Trigger Property="InputMode" Value="Mouse">
<Setter Property="Background" Value="Wheat" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:TestControl}">
<Grid Background="{TemplateBinding Background}">
<TextBlock HorizontalAlignment="Center"><Run Text="{TemplateBinding Text}" /></TextBlock>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="InputMode" Value="Touch">
<Setter Property="Background" Value="LightSkyBlue" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:TestControl}">
<Grid Background="{TemplateBinding Background}">
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Image
Source="{TemplateBinding ImageUri}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Width="{TemplateBinding ImageWidth}"
Height="{TemplateBinding ImageHeight}"
/>
<TextBlock HorizontalAlignment="Center"><Run Text="{TemplateBinding Text}" /></TextBlock>
</StackPanel>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<local:TestControl
ImageHeight="100"
ImageWidth="100"
Text="This is the test control"
ImageUri="http://www.optimizeagency.com/wp-content/uploads/2015/09/GoogleLogo.jpg"
/>
</Grid>
</Window>
C#:
using System;
using System.Windows;
using System.Windows.Controls;
namespace TestApplication
{
class TestControl : Control
{
public TestControl()
{
// If input mode may change at runtime, you'll need an event that fires when that
// happens and updates this property.
// UIUtilities.GetInputMode() is just a stub in this example.
InputMode = UIUtilities.GetInputMode();
}
#region InputMode Property
public InputMode InputMode
{
get { return (InputMode)GetValue(InputModeProperty); }
set { SetValue(InputModeProperty, value); }
}
public static readonly DependencyProperty InputModeProperty =
DependencyProperty.Register("InputMode", typeof(InputMode), typeof(TestControl),
new PropertyMetadata(InputMode.Mouse));
#endregion InputMode Property
#region Text Property
public String Text
{
get { return (String)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(String), typeof(TestControl),
new PropertyMetadata(null));
#endregion Text Property
#region ImageUri Property
// The TemplateBinding in the template can't coerce a string to an
// ImageSource, so we have to make that happen elsewhere.
public ImageSource ImageUri
{
get { return (ImageSource)GetValue(ImageUriProperty); }
set { SetValue(ImageUriProperty, value); }
}
public static readonly DependencyProperty ImageUriProperty =
DependencyProperty.Register("ImageUri", typeof(ImageSource), typeof(TestControl),
new PropertyMetadata(null));
#endregion ImageUri Property
#region ImageHeight Property
public float ImageHeight
{
get { return (float)GetValue(ImageHeightProperty); }
set { SetValue(ImageHeightProperty, value); }
}
public static readonly DependencyProperty ImageHeightProperty =
DependencyProperty.Register("ImageHeight", typeof(float), typeof(TestControl),
new PropertyMetadata(float.NaN));
#endregion ImageHeight Property
#region ImageWidth Property
public float ImageWidth
{
get { return (float)GetValue(ImageWidthProperty); }
set { SetValue(ImageWidthProperty, value); }
}
public static readonly DependencyProperty ImageWidthProperty =
DependencyProperty.Register("ImageWidth", typeof(float), typeof(TestControl),
new PropertyMetadata(float.NaN));
#endregion ImageWidth Property
}
#region This stuff belongs in a different file
public static class UIUtilities
{
public static InputMode GetInputMode()
{
// Here you'd do whatever you're already doing to detect the input mode
return InputMode.Touch;
}
}
public enum InputMode
{
Mouse,
Touch
}
#endregion This stuff belongs in a different file
}

Update popup position based on parent position

I want to update my Popup's position when the size of its parent is changing.
The following code works but there's a problem.
As you can see, inside the popup there's a big button (width 300), before the textbox reach this size, it don't update the popup position (try it yourself - write a super big sentence and you will see)
<TabControl>
<TabControl.ItemContainerStyle>
<Style TargetType="{x:Type TabItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabItem}">
<Grid Height="26"
Background="{TemplateBinding Background}"
x:Name="TabGrid">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter x:Name="tabTitle" Margin="5,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
ContentSource="Header"/>
<StackPanel Grid.Column="1" Height="26" Margin="0,0,1,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Horizontal">
<ToggleButton x:Name="Edit" Width="16" Content="e"
ToolTip="Edit" />
<Popup AllowsTransparency="True"
IsOpen="{Binding IsChecked, ElementName=Edit}"
Placement="Right"
PlacementTarget="{Binding ElementName=TabGrid}"
StaysOpen="False"
VerticalOffset="30"
HorizontalOffset="-20">
<Grid x:Name="PopupGrid">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Border Width="16" Height="3" Margin="0,0,20,0"
HorizontalAlignment="Right"
Panel.ZIndex="1" Background="White" />
<Border Grid.Row="1" Margin="0,-2,0,0"
Background="White"
BorderBrush="{Binding TabColor}"
BorderThickness="2">
<StackPanel>
<TextBox Name="Text"
Text="{Binding Content, ElementName=tabTitle, UpdateSourceTrigger=PropertyChanged}"
Margin="10"/>
<Button Width="300"/>
</StackPanel>
</Border>
</Grid>
</Popup>
</StackPanel>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</TabControl.ItemContainerStyle>
<TabItem Header="TabItem">
<Grid Background="#FFE5E5E5"/>
</TabItem>
<TabItem Header="TabItem">
<Grid Background="#FFE5E5E5"/>
</TabItem>
</TabControl>
Issue is PopUp needs some notification to update itself. So, as a workaround you can follow these steps to get it updated.
Hook TextChanged event for TextBox where you can raise some notification to popUp to update itself.
Next challenge would be how to get to popUp instance from TextBox. For that we can store the popUp in Tag of TextBox which we can access from code.
One of the property which results in re-calculation of popUp placement is VerticalOffset which we can set manually to force popUp to recalculate position.
That being said here is the code (updated XAML code):
<Popup x:Name="popUp" AllowsTransparency="True"
IsOpen="{Binding IsChecked, ElementName=Edit}" Placement="Right"
PlacementTarget="{Binding ElementName=TabGrid}" StaysOpen="False"
VerticalOffset="30" HorizontalOffset="-20">
<Grid x:Name="PopupGrid">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Border Width="16" Height="3" Margin="0,0,20,0" HorizontalAlignment="Right"
Panel.ZIndex="1" Background="White" />
<Border Grid.Row="1" Margin="0,-2,0,0" Background="White" BorderBrush="Black"
BorderThickness="2">
<StackPanel>
<TextBox Name="Text" TextChanged="Text_TextChanged"
Tag="{Binding ElementName=popUp}"
Text="{Binding Content, ElementName=tabTitle,
UpdateSourceTrigger=PropertyChanged}" Margin="10"/>
<Button Width="300"/>
</StackPanel>
</Border>
</Grid>
</Popup>
Code behind:
private void Text_TextChanged(object sender, TextChangedEventArgs e)
{
Popup popup = ((TextBox)sender).Tag as Popup;
if (popup != null)
{
popup.VerticalOffset += 1;
popup.VerticalOffset -= 1;
}
}
I created a behavior for your Popup, using the reflection solution from this question. I'm not sure whether that is a right solution to you... I tested the implementation on .NET3.5 without any issues.
Add the PopupBehavior class to your project:
/// <summary>
/// Attaches alignment behavior to a Popup element.
/// </summary>
public class PopupBehavior : Behavior<Popup>
{
#region Public fields
public static readonly DependencyProperty HeaderWidthProperty = DependencyProperty.Register("HeaderWidth", typeof(double), typeof(PopupBehavior), new FrameworkPropertyMetadata(0.0, HeaderWidthChanged));
public static readonly DependencyProperty PopupConnectionOffsetProperty = DependencyProperty.Register("PopupConnectionOffset", typeof(double), typeof(PopupBehavior), new FrameworkPropertyMetadata(0.0));
#endregion Public fields
#region Private fields
private MethodInfo updateMethod;
#endregion Private fields
#region Public properties
/// <summary>
/// Gets or sets the Width of the control to subscribe for changes.
/// </summary>
public double HeaderWidth
{
get { return (double)GetValue(HeaderWidthProperty); }
set { SetValue(HeaderWidthProperty, value); }
}
/// <summary>
/// Gets or sets the offset of the connection visual of the popup.
/// </summary>
public double PopupConnectionOffset
{
get { return (double)GetValue(PopupConnectionOffsetProperty); }
set { SetValue(PopupConnectionOffsetProperty, value); }
}
#endregion Public properties
#region Public constructors
/// <summary>
/// Creates an instance of the <see cref="PopupBehavior" /> class.
/// </summary>
public PopupBehavior()
{
updateMethod = typeof(Popup).GetMethod("Reposition", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
}
#endregion Public constructors
#region Protected methods
/// <summary>
/// Called after the behavior is attached to an AssociatedObject.
/// </summary>
protected override void OnAttached()
{
base.OnAttached();
var pd = DependencyPropertyDescriptor.FromProperty(Popup.IsOpenProperty, typeof(Popup));
pd.AddValueChanged(this.AssociatedObject, IsOpenChanged);
}
#endregion Protected methods
#region Private methods
/// <summary>
/// The HeaderWidth property has changed.
/// </summary>
private static void HeaderWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var b = d as PopupBehavior;
if (b != null)
b.UpdateHorizontalOffset();
}
/// <summary>
/// Gets the width of the associated popup.
/// </summary>
/// <returns>A double value; width of the popup.</returns>
/// <remarks>
/// This method gets the width of the popup's child, since the popup itself has a width of 0
/// when collapsed.
/// </remarks>
/// <exception cref="InvalidOperationException">
/// Occurs when the child of the popup is not derived from FrameworkElement.
/// </exception>
private double GetPopupWidth()
{
var child = this.AssociatedObject.Child as FrameworkElement;
if (child != null)
return child.ActualWidth;
else
throw new InvalidOperationException("Child of Popup is not derived from FrameworkElement");
}
/// <summary>
/// The IsOpen property of the popup has changed.
/// </summary>
private void IsOpenChanged(object sender, EventArgs e)
{
if (this.AssociatedObject.IsOpen)
UpdateHorizontalOffset();
}
/// <summary>
/// Updates the HorizontalOffset of the popup.
/// </summary>
private void UpdateHorizontalOffset()
{
if (this.AssociatedObject.IsOpen)
{
var offset = (GetPopupWidth() - PopupConnectionOffset) * -1;
if (this.AssociatedObject.HorizontalOffset == offset)
updateMethod.Invoke(this.AssociatedObject, null);
else
this.AssociatedObject.HorizontalOffset = offset;
}
}
#endregion Private methods
}
Make some changes in your XAML:
Change the Placement property to "Bottom".
Bind the PlacementTarget to your button (Edit) .
Remove the HorizontalOffset and VerticalOffset properties from your Popup.
Assign the Behavior.
Your code should look something like this:
...
xmlns:local="clr-namespace:MyProject"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
...
<Popup
AllowsTransparency="True"
IsOpen="{Binding IsChecked, ElementName=Edit}"
Placement="Bottom"
PlacementTarget="{Binding ElementName=Edit}"
StaysOpen="False">
<i:Interaction.Behaviors>
<local:PopupBehavior
HeaderWidth="{Binding ActualWidth, ElementName=tabTitle}"
PopupConnectionOffset="36" />
</i:Interaction.Behaviors>
...
</Popup>
Change the "local" clr-namespace to that of your project. If not already using, you might need to add a reference to the System.Windows.Interactivity assembly, I installed it using NuGet (Blend.Interactivity.Wpf).
The HeaderWidth purely subscribes to the changes of the header width of the TabItem and PopupConnectionOffset defines the offset from the right side of the Popup to the left side of the 'white stripe Border'.
Note that FrameworkElement should be assignable from the Popup's child value. Also, since the Popup is now targetted to the Edit button instead of the TabGrid, it aligns correctly when the TabItems exceed the parent container, preventing situations as these:

TemplateBinding in a nested template in Silverlight 4

I've implemented a control, CommandTextBox, which I want to be a text box with a button right next to it (so it almost appears within the text box).
The button should be an image which I can bind to an icon. It's fairly straightfoward stuff...
public class CommandTextBox : TextBox
{
/// <summary>
/// The image property.
/// </summary>
public static readonly DependencyProperty ImageProperty = DependencyProperty.Register(
"Image", typeof(ImageSource), typeof(CommandTextBox), null);
/// <summary>
/// Initializes a new instance of the <see cref = "CommandTextBox" /> class.
/// </summary>
public CommandTextBox()
{
this.DefaultStyleKey = typeof(CommandTextBox);
}
/// <summary>
/// Gets or sets the image.
/// </summary>
/// <value>
/// The image.
/// </value>
public ImageSource Image
{
get
{
return (ImageSource)this.GetValue(ImageProperty);
}
set
{
this.SetValue(ImageProperty, value);
}
}
}
I have a template as follows...
<Style TargetType="Controls:CommandTextBox">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Controls:CommandTextBox">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Text="{TemplateBinding Text}"/>
<Button Grid.Column="1"
Content="Search" >
<Button.Template>
<ControlTemplate>
<Image Source="{TemplateBinding Image}" />
</ControlTemplate>
</Button.Template>
</Button>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
But I get an error due to the template binding in the image. I sortof understand why, it's because the Template has changed now so the binding context isn't the same but I don't know how to overcome it.
Do I need to create a seperate ImageButton control so I can just do a normal template binding or is there another way?
Thanks
Ben
I have managed to get this to work by changing the style as follows:
<Style TargetType="appControls:CommandTextBox">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="appControls:CommandTextBox">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Text="{TemplateBinding Text}"/>
<Button Grid.Column="1" >
<Button.Content>
<Image DataContext="{TemplateBinding Image}" Source="{Binding}" />
</Button.Content>
</Button>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
I'm not using a seperate template for the Button. My XAML with the control is:
<controls:CommandTextBox Text="Text" Image="/MyApp.Silverlight;component/Assets/Images/Amber_Triangle.png"></controls:CommandTextBox>
This appears to achieve the result you were after. The control renders on my test page as expected.

Resources