In my MainWindow, there's ContentControl defined like this:
<ContentControl x:Name="rectangle" Width="150" Height="150" Canvas.Top="150" Canvas.Left="470" Selector.IsSelected="True" Style="{StaticResource DesignerItemStyle}">
<Rectangle Fill="Blue" Stretch="Fill" IsHitTestVisible="False" />
</ContentControl>
and the DesignerItem.xaml is (which used to class called MoveThumb and RotateThumb:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:dr="clr-namespace:DragAndRotate">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="MoveThumb.xaml" />
<ResourceDictionary Source="RotateDecorator.xaml" />
</ResourceDictionary.MergedDictionaries>
<Style x:Key="DesignerItemStyle" TargetType="ContentControl">
<Setter Property="MinHeight" Value="50" />
<Setter Property="MinWidth" Value="50" />
<Setter Property="RenderTransformOrigin" Value="0.5,0.5" />
<Setter Property="SnapsToDevicePixels" Value="true"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ContentControl">
<Grid DataContext="{Binding RelativeSource={RelativeSource TemplatedParent}}">
<Control Name="RotateDecorator"
Template="{StaticResource RotateDecoratorTemplate}"
Visibility="Collapsed" />
<dr:MoveThumb Template="{StaticResource MoveThumbTemplate}" Cursor="SizeAll" />
<ContentPresenter Content="{TemplateBinding ContentControl.Content}" />
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="Selector.IsSelected" Value="True">
<Setter TargetName="RotateDecorator" Property="Visibility" Value="Visible" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
And I want to get the angle of rectangle which will be changed during rotating and binding it to a textbox in MainWindow. I tried a global variable by creating a new class called bridge implements INotifyPropertyChanged including the real time changed variable by the RotateThumb and bind it to TextBox in MainWindow but it doesn't work, which is also not elegant in my opinion.
Is there any way to get the changing variable in class RotateThumb and bind it to TextBox? Thank you in advance.
RotateDecorator.xaml just provides metaphor to show the Control can be drag to rotate. And below is RotateThumb and I wanna rotateTransform.Angle changed in DragDelta:
public class RotateThumb : Thumb
{
private Point centerPoint;
private Vector startVector;
private double initialAngle;
private Canvas designerCanvas;
private ContentControl designerItem;
private RotateTransform rotateTransform;
public RotateThumb()
{
DragDelta += new DragDeltaEventHandler(RotateThumb_DragDelta);
DragStarted += new DragStartedEventHandler(RotateThumb_DragStarted);
}
private void RotateThumb_DragStarted(object sender, DragStartedEventArgs e)
{
this.designerItem = DataContext as ContentControl;
if (this.designerItem != null)
{
this.designerCanvas = VisualTreeHelper.GetParent(designerItem) as Canvas;
if (this.designerCanvas != null)
{
this.centerPoint = this.designerItem.TranslatePoint(new Point(this.designerItem.Width * this.designerItem.RenderTransformOrigin.X, this.designerItem.Height * this.designerItem.RenderTransformOrigin.Y), this.designerCanvas);
Point startPoint = Mouse.GetPosition(this.designerCanvas);
this.startVector = Point.Subtract(startPoint, this.centerPoint);
this.rotateTransform = this.designerItem.RenderTransform as RotateTransform;
if (this.rotateTransform == null)
{
this.designerItem.RenderTransform = new RotateTransform(0);
this.initialAngle = 0;
}
else
this.initialAngle = this.rotateTransform.Angle;
}
}
}
void RotateThumb_DragDelta(object sender, DragDeltaEventArgs e)
{
if (this.designerItem != null && this.designerCanvas != null)
{
Point current = Mouse.GetPosition(this.designerCanvas);
Vector deltaVector = Point.Subtract(current, this.centerPoint);
double angle = Vector.AngleBetween(this.startVector, deltaVector);
RotateTransform rotateTransform = this.designerItem.RenderTransform as RotateTransform;
rotateTransform.Angle = this.initialAngle + Math.Round(angle, 0);
this.designerItem.InvalidateMeasure();
}
}
}
And below is the Bridge, 'cause I don't how to angle in RotateThumb, so I create "bridge" and hope MainWindow.Textbox's context can be bind to it:
public class Bridge : INotifyPropertyChanged
{
private string angletext;
public string Angle
{
get { return angletext; }
set
{
if (value != angletext)
{
angletext = value;
Console.WriteLine(angletext);
OnPropertyChanged(new PropertyChangedEventArgs("Angle"));
}
}
}
public void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
{
PropertyChanged(this, e);
}
}
public event PropertyChangedEventHandler PropertyChanged;
public static Bridge bridge = new Bridge();
Your's is one of those cases where a normal binding is somewhat useless.
In the RotateThumb class, I'd expose a custom tailored RoutedCommand which fires a command carrying the current angle on every thumb change. Since the RoutedCommand bubbles up the visual tree, it's quite easy to catch the incoming notifications in the Window.
The only (relative) problem is to get notified as soon the visual tree is completely built. Since there's no (supposed) thumb value's change, the owning window won't be notified. However, you may assume that the initial (default) thumb value is known by the window.
Hope it helps.
Related
I want an expander to expand if a flag in the VM is set. I also want the user to be able to override this and expand/collapse at will. The following code doesn't work, the timer kicks in and the expander expands and collapses repeatedly - then If you click the expander manually it swiches too - but the trigger fails to expand or collapse the expander. Its of course as if the manually keyed value is set and is taking priority over the Trigger Setter.
<Expander Header="Test" BorderThickness="2" BorderBrush="Black" VerticalAlignment="Bottom">
<Expander.Style >
<Style TargetType="Expander">
<Setter Property="IsExpanded" Value="True"></Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding DataContext.AmSet,
RelativeSource={RelativeSource AncestorType=Grid}}"
Value="True">
<Setter Property="IsExpanded" Value="False"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</Expander.Style>
<Expander.Content>
<Border Background="AliceBlue" Width="50" Height="50"></Border>
</Expander.Content>
The VM has a dummy timer that just switches the flag to trigger the update as below
public class vm : INotifyPropertyChanged
{
public vm()
{
t = new System.Timers.Timer(1000);
t.Elapsed += t_Elapsed;
t.Start();
}
bool _AmSet = false;
public bool AmSet
{
get { return _AmSet; }
set
{
_AmSet = value;
OnPropertyChanged("");
}
}
void t_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
AmSet = !AmSet;
}
System.Timers.Timer t;
private void OnPropertyChanged(string prop)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(prop));
}
public event PropertyChangedEventHandler PropertyChanged;
}
Is there a reason you need to do this with a DataTrigger? It could be achieved easily with a two-way binding.
<Expander Header="Test" BorderThickness="2" BorderBrush="Black" VerticalAlignment="Bottom" IsExpanded="{Binding AmSet, Mode=TwoWay}"/>
I created an expander style that contains a checkbox in its header. The checkbox state is bound to an attached property:
<Style TargetType="{x:Type Expander}" x:Key="MyCheckboxExpander">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Expander}">
(...)
<CheckBox x:Name="ExpanderHeaderChk" VerticalAlignment="Center" Margin="4,0,0,2"
IsChecked="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(my:AP.IsChecked)}" />
(...)
I my view, inside the expander I have a stackpanel with a ComboBox.
Whenever the user checks the expander's checkbox, I wan't that the combobox gets the first item selected, on the oher hand whenever the user unchecks it, I wan't that the selecteditem of the combobox be null.
How can I accomplish this? I'm following the MVVM pattern, but since this is more a matter of the view, I'm open to code-behind suggestions.
Well, I think your design is not optimal. You see, you are trying to change the semantics of the Expander. The real expander doesn't have the semantics with additional checkbox, so the control you are creating is not an Expander any more.
I would suggest that you switch to a user control (or maybe a custom control, look at your semantics), and expose the needed event in your control's class. The XAML for the user control should be perhaps an expander with a checkbox.
Edit: example with UserControl (not tested)
(XAML)
<UserControl x:Class="namespace:MyCheckboxExpander">
<Expander>
...
<Checkbox x:Name="cb"/>
...
</Expander>
</UserControl>
(code-behind)
public class MyCheckboxExpander : UserControl
{
MyCheckboxExpander()
{
InitializeComponent();
cb.Check += OnCheck;
}
void OnCheck(object sender, whatever2 args)
{
if (CheckboxTriggered != null)
CheckboxTriggered(new EventArgs<whatever>);
}
public event EventArgs<whatever> CheckboxTriggered;
}
WPF is so powerfull framework, that you can solve you problem just using next style for Expander:
<Style x:Key="myExpanderStyle" TargetType="{x:Type Expander}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Expander}">
<StackPanel>
<CheckBox x:Name="PART_CheckBox" IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" />
<ComboBox x:Name="PART_ComboBox" ItemsSource="{TemplateBinding Content}" />
</StackPanel>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="True">
<Setter TargetName="PART_ComboBox" Property="SelectedIndex" Value="0"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
SAMPLE:
<Expander Style="{StaticResource myExpanderStyle}">
<x:Array Type="sys:String">
<sys:String>1</sys:String>
<sys:String>2</sys:String>
<sys:String>3</sys:String>
</x:Array>
</Expander>
Just XAML! I like XAML declarativity.
But from MVVM perspective, this approach has one disadvantage - I can't cover this case with unit tests. So, I would prefer:
create view model with properties: IsChecked(bound to CheckBox),
SelectedItem(bound to ComboBox) and Source(ItemsSource for ComboBox) -
abstration of my real view without any references on controls;
write a logic in view model that set or unset SelectedItem depending
on IsChecked property;
cover that logic with unit test (yep, you can
even start with this point, if you like test first approach).
I followed the suggestion provided by #Baboon and I created a custom control with a routed event named CheckedChanged, this way I can access it through the view's xaml and code-behind:
[TemplatePart(Name = "PART_Expander", Type = typeof(Expander))]
[TemplatePart(Name = "PART_CheckBox", Type = typeof(CheckBox))]
public class MyCustomExpander : Expander
{
static MyCustomExpander()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MyCustomExpander), new FrameworkPropertyMetadata(typeof(MyCustomExpander)));
}
public bool IsChecked
{
get { return (bool)GetValue(IsCheckedProperty); }
set { SetValue(IsCheckedProperty, value); }
}
public static readonly DependencyProperty IsCheckedProperty =
DependencyProperty.Register("IsChecked", typeof(bool), typeof(MyCustomExpander),
new UIPropertyMetadata(false));
#region Events
private CheckBox chkExpander = new CheckBox();
public CheckBox ChkExpander { get { return chkExpander; } private set { chkExpander = value; } }
public static readonly RoutedEvent CheckedChangedEvent = EventManager.RegisterRoutedEvent("ExtraButtonClick",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(MyCustomExpander));
public event RoutedEventHandler CheckedChanged
{
add { AddHandler(CheckedChangedEvent, value); }
remove { RemoveHandler(CheckedChangedEvent, value); }
}
void OnCheckedChanged(object sender, RoutedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(CheckedChangedEvent, this));
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
CheckBox chk = base.GetTemplateChild("PART_CheckBox") as CheckBox;
if (chk != null)
{
chk.Checked += new RoutedEventHandler(OnCheckedChanged);
chk.Unchecked += new RoutedEventHandler(OnCheckedChanged);
}
}
#endregion
}
I want to thank to #Baboon and #Vlad for their help.
I am trying to create a templated button control with databinding for the Visibility, tooltip, and Command. The Visibility binding works, as does the tooltip, but the Command does not. Another process is responsible for injecting the viewmodel and associating it with the View, and the other data bindings are working so I am pretty confident that is working properly.
In the resource dictionary:
<Converters:BoolToVisibilityConverter x:Key="boolVisibilityConverter" />
<Style TargetType="local:ImageButton">
<Setter Property="Visibility" Value="{Binding FallbackValue=Visible, Path=ToolIsAvailable, Converter={StaticResource boolVisibilityConverter} }"/>
<Setter Property="Command" Value="{Binding ButtonCommand}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:ImageButton">
<Grid>
<Image Source="{TemplateBinding Image}"
ToolTipService.ToolTip="{Binding ToolName}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
the templated control
public class MyButton: ImageButton
{
public MyButton(MyCommandViewModel viewmodel)
{
this.DefaultStyleKey = typeof(ImageButton);
this.Image = new BitmapImage(new Uri("/MyProject;component/Themes/myimage.png", UriKind.Relative));
this.DataContext = viewmodel;
}
}
and in the view model
public MyCommandViewModel()
: base("My Tool", true)
{
}
public class CommandViewModel
{
public CommandViewModel(string toolName, bool isAvailable)
{
ToolIsAvailable = isAvailable;
ToolName = toolName;
_buttoncommand = new DelegateCommand(() =>
{
ExecuteCommand();
},
() => { return CanExecute; });
}
private bool _canExecute = true;
public bool CanExecute
{
get { return _canExecute; }
set
{
_canExecute = value;
OnPropertyChanged("CanExecute");
if (_command != null) _command.RaiseCanExecuteChanged();
}
}
private DelegateCommand _buttoncommand;
public ICommand ButtonCommand
{
get { return _buttoncommand; }
}
protected virtual void ExecuteCommand()
{
}
public bool ToolIsAvailable
{
get { return _toolIsReady; }
set { _toolIsReady = value; OnPropertyChanged("ToolIsAvailable"); }
}
public string ToolName
{
get { return _toolName; }
set { _toolName = value; OnPropertyChanged("ToolName"); }
}
}
Why are the other databindings functioning properly but not the Command data binding. I found this similar post
Overriding a templated Button's Command in WPF
Do I need to template a grid control instead and use RoutedCommands? I am not sure I understand why Silverlight treats the Command binding different than the others so I suspect I just have a bug in the code.
Does specifically looking for the datacontext work?
Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}, Path=DataContext.ButtonCommand}"
This was my solution. Using the same commandviewmodel as above and same MyCommandViewModel
<Style TargetType="local:ImageButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:ImageButton">
<Grid>
<Image Source="{TemplateBinding Image}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The databinding is now done in a user control
<UserControl x:Class="SilverlightApplication11.Test"
...
>
<UserControl.Resources>
<Converters:BoolToVisibilityConverter x:Key="boolVisibilityConverter" />
</UserControl.Resources>
<Grid>
<local:ImageButton Image="/SilverlightApplication11;component/Themes/hand.png" Command="{Binding ButtonCommand}" Visibility="{Binding FallbackValue=Visible, Path=ToolIsAvailable, Converter={StaticResource boolVisibilityConverter} }"/>
</Grid>
</UserControl>
and the code behind
public Test(TestCommandViewModel vm)
{
InitializeComponent();
this.Loaded += (o, e) => this.DataContext = vm;
}
I'm making a touchscreen interface that uses a listbox.
I have a button above and below the listbox for page up/down.
I'm trying to get it to where when scrolled all the way up the pageup button gets disabled.
and when scrolled all the way down the pagedown button gets disabled too.
Here's the code in my Styles.xaml for the Listbox
<Style x:Key="{x:Type ListBox}" TargetType="{x:Type ListBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate x:Key="{x:Type ListBox}" TargetType="{x:Type ListBox}">
<DockPanel>
<RepeatButton x:Name="LineUpButton" DockPanel.Dock="Top"
HorizontalAlignment="Stretch"
Height="50"
Content="/\"
Command="{x:Static ScrollBar.PageUpCommand}"
CommandTarget="{Binding ElementName=scrollviewer}" />
<RepeatButton x:Name="LineDownButton" DockPanel.Dock="Bottom"
HorizontalAlignment="Stretch"
Height="50"
Content="\/"
Command="{x:Static ScrollBar.PageDownCommand}"
CommandTarget="{Binding ElementName=scrollviewer}" />
<Border BorderThickness="1" BorderBrush="Gray" Background="White">
<ScrollViewer x:Name="scrollviewer">
<ItemsPresenter/>
</ScrollViewer>
</Border>
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Hidden"/>
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Hidden"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
</Style>
And here's where I instantiate the listbox
<ListBox SelectedItem="{Binding SelectedCan}" ItemsSource="{Binding Path=SelectedKioskCashCans}">
<ListBox.ItemTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding image}" MaxWidth="75" />
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
I searched all around yesterday with no luck.
I'm hoping to be able to do it all in xaml.
I'm using images for the buttons but took them out for readability above,
they really look like...
<RepeatButton x:Name="LineUpButton" DockPanel.Dock="Top" HorizontalAlignment="Stretch"
Height="50"
Command="{x:Static ScrollBar.PageUpCommand}"
CommandTarget="{Binding ElementName=scrollviewer}">
<RepeatButton.Template>
<ControlTemplate TargetType="{x:Type RepeatButton}">
<Grid>
<Image Name="Normal" Source="/Images/up.png"/>
<Image Name="Pressed" Source="/Images/up.png" Visibility="Hidden"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="Normal" Property="Visibility" Value="Hidden"/>
<Setter TargetName="Pressed" Property="Visibility" Value="Visible"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</RepeatButton.Template>
</RepeatButton>
Just use CanExecute method of the PageUpCommand for that. Return false if where are no pages left and the button will become disabled automatically.
EDIT:
I have created a simple attached behavior that can be used to fix this problem. Just set the following attached property on the ScrollViewer:
<ScrollViewer x:Name="scrollviewer"
z:ScrollBarCommandsCanExecuteFixBehavior.IsEnabled="True">
<ItemsPresenter/>
</ScrollViewer>
And here is the source code of the behavior:
public static class ScrollBarCommandsCanExecuteFixBehavior
{
#region Nested Types
public class CommandCanExecuteMonitor<T> where T : UIElement
{
protected T Target { get; private set; }
protected CommandCanExecuteMonitor(T target, RoutedCommand command)
{
Target = target;
var binding = new CommandBinding(command);
binding.CanExecute += OnCanExecute;
target.CommandBindings.Add(binding);
}
protected virtual void OnCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
}
}
public class PageUpCanExecuteMonitor : CommandCanExecuteMonitor<ScrollViewer>
{
public PageUpCanExecuteMonitor(ScrollViewer scrollViewer)
: base(scrollViewer, ScrollBar.PageUpCommand)
{
}
protected override void OnCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
if (e.Handled)
{
return;
}
if (Equals(Target.VerticalOffset, 0.0))
{
e.CanExecute = false;
e.Handled = true;
}
}
}
public class PageDownCanExecuteMonitor : CommandCanExecuteMonitor<ScrollViewer>
{
public PageDownCanExecuteMonitor(ScrollViewer scrollViewer)
: base(scrollViewer, ScrollBar.PageDownCommand)
{
}
protected override void OnCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
if (e.Handled)
{
return;
}
if (Equals(Target.VerticalOffset, Target.ScrollableHeight))
{
e.CanExecute = false;
e.Handled = true;
}
}
}
#endregion
#region IsEnabled Attached Property
public static bool GetIsEnabled(DependencyObject obj)
{
return (bool) obj.GetValue(IsEnabledProperty);
}
public static void SetIsEnabled(DependencyObject obj, bool value)
{
obj.SetValue(IsEnabledProperty, value);
}
public static readonly DependencyProperty IsEnabledProperty =
DependencyProperty.RegisterAttached("IsEnabled", typeof (bool), typeof (ScrollBarCommandsCanExecuteFixBehavior), new PropertyMetadata(false, OnIsEnabledChanged));
private static void OnIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if ((bool) e.NewValue)
{
var scrollViewer = d as ScrollViewer;
if (scrollViewer != null)
{
OnAttached(scrollViewer);
}
else
{
throw new NotSupportedException("This behavior only supports ScrollViewer instances.");
}
}
}
private static void OnAttached(ScrollViewer target)
{
SetPageUpCanExecuteMonitor(target, new PageUpCanExecuteMonitor(target));
SetPageDownCanExecuteMonitor(target, new PageDownCanExecuteMonitor(target));
}
#endregion
#region PageUpCanExecuteMonitor Attached Property
private static void SetPageUpCanExecuteMonitor(DependencyObject obj, PageUpCanExecuteMonitor value)
{
obj.SetValue(PageUpCanExecuteMonitorProperty, value);
}
private static readonly DependencyProperty PageUpCanExecuteMonitorProperty =
DependencyProperty.RegisterAttached("PageUpCanExecuteMonitor", typeof (PageUpCanExecuteMonitor), typeof (ScrollBarCommandsCanExecuteFixBehavior), new PropertyMetadata(null));
#endregion
#region PageDownCanExecuteMonitor Attached Property
private static void SetPageDownCanExecuteMonitor(DependencyObject obj, PageDownCanExecuteMonitor value)
{
obj.SetValue(PageDownCanExecuteMonitorProperty, value);
}
private static readonly DependencyProperty PageDownCanExecuteMonitorProperty =
DependencyProperty.RegisterAttached("PageDownCanExecuteMonitor", typeof (PageDownCanExecuteMonitor), typeof (ScrollBarCommandsCanExecuteFixBehavior), new PropertyMetadata(null));
#endregion
}
The basic idea is that we add a CommandBinding to the ScrollViewer for each of the commands and subscribe to the CanExecute event on those bindings. In the event handler we check the current position of the scroll and set the e.CanExecute property accordingly.
I have added a tooltip (shown below) to the track in the slider template, but rather than binding to the current value of the slider, I would like to bind to the value corresponding to the "track value" the mouse is over. Similar to what the youtube video slider allows. So the user can mouseover the track and also see the corresponding value, without having to actually move the thumb.
<Track Grid.Row="1" Name="PART_Track" ToolTip="{Binding Path=Value}" ToolTipService.Placement="Mouse">
</Track>
Any ideas? Thanks!
I have created an attached behaviour that will find the Track from a slider and subscribe to its MouseMove event to set the tooltip of the track to the corresponding value of the tick the mouse is over. I also added a Prefix property so you can write what the value is:
internal class ShowTickValueBehavior : Behavior<Slider>
{
private Track track;
public static readonly DependencyProperty PrefixProperty = DependencyProperty.Register(
"Prefix",
typeof(string),
typeof(ShowTickValueBehavior),
new PropertyMetadata(default(string)));
public string Prefix
{
get
{
return (string)this.GetValue(PrefixProperty);
}
set
{
this.SetValue(PrefixProperty, value);
}
}
protected override void OnAttached()
{
this.AssociatedObject.Loaded += this.AssociatedObjectOnLoaded;
base.OnAttached();
}
protected override void OnDetaching()
{
this.track.MouseMove -= this.TrackOnMouseMove;
this.track = null;
base.OnDetaching();
}
private void AssociatedObjectOnLoaded(object sender, RoutedEventArgs routedEventArgs)
{
this.AssociatedObject.Loaded -= this.AssociatedObjectOnLoaded;
this.track = (Track)this.AssociatedObject.Template.FindName("PART_Track", this.AssociatedObject);
this.track.MouseMove += this.TrackOnMouseMove;
}
private void TrackOnMouseMove(object sender, MouseEventArgs mouseEventArgs)
{
var position = mouseEventArgs.GetPosition(this.track);
var valueFromPoint = this.track.ValueFromPoint(position);
var floorOfValueFromPoint = (int)Math.Floor(valueFromPoint);
var toolTip = string.Format(CultureInfo.InvariantCulture, "{0}{1}", this.Prefix, floorOfValueFromPoint);
ToolTipService.SetToolTip(this.track, toolTip);
}
}
Usage
<Window x:Class="TestSlider.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-TestSlider"
xmlns:system="clr-namespace:System;assembly=mscorlib"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
>
<Grid>
<Slider Name="Slider1"
IsSnapToTickEnabled="True"
TickFrequency="1"
TickPlacement="BottomRight"
IsMoveToPointEnabled="True"
Minimum="13"
Maximum="25"
>
<i:Interaction.Behaviors>
<local:ShowTickValueBehavior Prefix="Volume: "/>
</i:Interaction.Behaviors>
</Slider>
</Grid>
Result:
I'd imagine you're going to have to create a new control, inheriting from the slider. You'd need to implement mousein/out and mousemove, calculate the value based on mouse offset and change the tooltip.
I don't think there's any property you can use "out of the box" so the calculation may be rather tricky if you need to factor in reskinning of margins etc.
First you need modify Slider control
<Slider VerticalAlignment="Center" HorizontalAlignment="Stretch">
<Slider.Template>
<ControlTemplate TargetType="{x:Type Slider}">
<Grid>
<xamlTest:ReflectablePopup x:Name="InfoPopup" Width="Auto" Height="Auto" PlacementTarget="{Binding ElementName=Thumb}" Placement="Top" StaysOpen="False" IsOpen="False" AllowsTransparency="True">
<Border Padding="2" CornerRadius="3" Background="#555C5C5C">
<Label Content="Your Text"></Label>
</Border>
</xamlTest:ReflectablePopup>
<Track x:Name="PART_Track">
<Track.Thumb>
<Thumb x:Name="Thumb" Width="10" Height="20">
</Thumb>
</Track.Thumb>
</Track>
</Grid>
<ControlTemplate.Triggers>
<Trigger SourceName="Thumb" Property="IsDragging" Value="True">
<Setter Value="True" TargetName="InfoPopup" Property="IsOpen" />
</Trigger>
<Trigger SourceName="Thumb" Property="IsDragging" Value="False">
<Setter Value="False" TargetName="InfoPopup" Property="IsOpen" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Slider.Template>
</Slider>
As you see, I using ReflectablePopup instead Popup. Because Popup can`t relocate after PlacementTarget moved.
Below the code fo ReflectablePopup (C#):
public class ReflectablePopup : Popup
{
protected override void OnOpened(EventArgs e)
{
var friend = this.PlacementTarget;
friend.QueryCursor += friend_QueryCursor;
base.OnOpened(e);
}
protected override void OnClosed(EventArgs e)
{
var friend = this.PlacementTarget;
friend.QueryCursor -= friend_QueryCursor;
base.OnClosed(e);
}
private void friend_QueryCursor(object sender, System.Windows.Input.QueryCursorEventArgs e)
{
this.HorizontalOffset += +0.1;
this.HorizontalOffset += -0.1;
}
}