How to disable the WPF ScrollViewer to refresh the view - wpf

I am using a stack panel for displaying a slideshow. The stack panel contains two images.
Each image is bound to the View Model containing among other properties the path to the image on the disk. In the general case there are more than 2 images in the list.
There is an endless while loop used for changing the current and next pointer to the image and in this way after the slide transition is finished the the current image becomes previous and the next becomes current.
The slide transition is made by changing the scroll viewer from 0 to 100% of scrollable extended area.
This works and even very fast. It is the easiest way to implement a slide transition for an endless list of images using MVVM pattern.
My problem is that from time to time the slide transition slips and doesn't finishes at exact 100% visually but the storyboard animation completed event is triggered.
In the ViewModel I am moving the Current and Next poitners and the image blinks. Some flickering is visible from time to time.
I need to find a way on how to temporarily disable the ScrollViewer to display any visual change when I am repositioning the scoller from 100% to 0%.
I need to invalidate the WPF ScrollViewer only temporarily until I change the scroll position from 100% to 0% but this must not be visible on screen.
I mean the values must be changed but the screen must be frozen.
Is it possible somehow ?
XAML
<UserControl.Resources>
<Storyboard x:Key="sbVerticalSlideEasingTransition"
AutoReverse="True" RepeatBehavior="Forever">
<Storyboard x:Key="sbVerticalSlideTransition"
FillBehavior="HoldEnd">
<!--SCROLL TO THE MULTIPLIER OF THE HEIGHT (BASICALLY AS THE PERCENTAGE)-->
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="Mediator"
Storyboard.TargetProperty="ScrollableHeightMultiplier">
<LinearDoubleKeyFrame KeyTime="00:0:0" Value="0" />
<LinearDoubleKeyFrame KeyTime="00:0:5" Value="1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</UserControl.Resources>
<Grid x:Name="gdContainer" Background="Black">
<ScrollViewer x:Name="Scroller" Visibility="{Binding Path=IsSlider, Mode=TwoWay, NotifyOnSourceUpdated=True, ElementName=slidePlayer,
Converter='{StaticResource BoolToVisibility}'}"
VerticalScrollBarVisibility="Hidden"
HorizontalScrollBarVisibility="Disabled">
<StackPanel x:Name="ScrollPanel">
<ctrl:MediaElementEx x:Name="slide1" Height="{Binding Path=ActualHeight, ElementName=gdContainer}" Width="{Binding Path=ActualWidth, ElementName=gdContainer}" />
<ctrl:MediaElementEx x:Name="slide2" Height="{Binding Path=ActualHeight, ElementName=gdContainer}" Width="{Binding Path=ActualWidth, ElementName=gdContainer}" />
</StackPanel>
</ScrollViewer>
<!-- Mediator that forwards the property changes -->
<cmn:ScrollViewerOffsetMediator x:Name="Mediator" ScrollViewer="{Binding ElementName=Scroller}"/>
</Grid>
ScrollViewer Mediator
This is used to bind the ScrollOffset of the scrollviewer that is not bindable directly
/// <summary>
/// Mediator that forwards Offset property changes on to a ScrollViewer
/// instance to enable the animation of Horizontal/VerticalOffset.
/// </summary>
public class ScrollViewerOffsetMediator : FrameworkElement
{
/// <summary>
/// ScrollViewer instance to forward Offset changes on to.
/// </summary>
public ScrollViewer ScrollViewer
{
get { return (ScrollViewer)GetValue(ScrollViewerProperty); }
set { SetValue(ScrollViewerProperty, value); }
}
public static readonly DependencyProperty ScrollViewerProperty =
DependencyProperty.Register(
"ScrollViewer",
typeof(ScrollViewer),
typeof(ScrollViewerOffsetMediator),
new PropertyMetadata(OnScrollViewerChanged));
private static void OnScrollViewerChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
var mediator = (ScrollViewerOffsetMediator)o;
var scrollViewer = (ScrollViewer)(e.NewValue);
if (null != scrollViewer)
{
scrollViewer.ScrollToVerticalOffset(mediator.VerticalOffset);
}
}
/// <summary>
/// VerticalOffset property to forward to the ScrollViewer.
/// </summary>
public double VerticalOffset
{
get { return (double)GetValue(VerticalOffsetProperty); }
set { SetValue(VerticalOffsetProperty, value); }
}
public static readonly DependencyProperty VerticalOffsetProperty =
DependencyProperty.Register(
"VerticalOffset",
typeof(double),
typeof(ScrollViewerOffsetMediator),
new PropertyMetadata(OnVerticalOffsetChanged));
public static void OnVerticalOffsetChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
var mediator = (ScrollViewerOffsetMediator)o;
if (null != mediator.ScrollViewer)
{
mediator.ScrollViewer.ScrollToVerticalOffset((double)(e.NewValue));
}
}
/// <summary>
/// HorizontalOffset property to forward to the ScrollViewer.
/// </summary>
public double HorizontalOffset
{
get { return (double)GetValue(HorizontalOffsetProperty); }
set { SetValue(VerticalOffsetProperty, value); }
}
public static readonly DependencyProperty HorizontalOffsetProperty =
DependencyProperty.Register(
"HorizontalOffset",
typeof(double),
typeof(ScrollViewerOffsetMediator),
new PropertyMetadata(OnHorizontalOffsetChanged));
public static void OnHorizontalOffsetChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
var mediator = (ScrollViewerOffsetMediator)o;
if (null != mediator.ScrollViewer)
{
mediator.ScrollViewer.ScrollToHorizontalOffset((double)(e.NewValue));
}
}
/// <summary>
/// Multiplier for ScrollableHeight property to forward to the ScrollViewer.
/// </summary>
/// <remarks>
/// 0.0 means "scrolled to top"; 1.0 means "scrolled to bottom".
/// </remarks>
public double ScrollableHeightMultiplier
{
get { return (double)GetValue(ScrollableHeightMultiplierProperty); }
set { SetValue(ScrollableHeightMultiplierProperty, value); }
}
public static readonly DependencyProperty ScrollableHeightMultiplierProperty =
DependencyProperty.Register(
"ScrollableHeightMultiplier",
typeof(double),
typeof(ScrollViewerOffsetMediator),
new PropertyMetadata(OnScrollableHeightMultiplierChanged));
public static void OnScrollableHeightMultiplierChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
var mediator = (ScrollViewerOffsetMediator)o;
var scrollViewer = mediator.ScrollViewer;
if (null != scrollViewer)
{
scrollViewer.ScrollToVerticalOffset((double)(e.NewValue) * scrollViewer.ScrollableHeight);
if (((double)e.NewValue == 0) && ((double)e.OldValue == 1))
{
Debug.WriteLine("[1] {0:HH:mm:ss.fff} ScrollableHeightMultiplier 1 => 0 ", DateTime.Now);
}
}
}
/// <summary>
/// Multiplier for ScrollableWidth property to forward to the ScrollViewer.
/// </summary>
/// <remarks>
/// 0.0 means "scrolled to top"; 1.0 means "scrolled to bottom".
/// </remarks>
public double ScrollableWidthMultiplier
{
get { return (double)GetValue(ScrollableWidthMultiplierProperty); }
set { SetValue(ScrollableWidthMultiplierProperty, value); }
}
public static readonly DependencyProperty ScrollableWidthMultiplierProperty =
DependencyProperty.Register(
"ScrollableWidthMultiplier",
typeof(double),
typeof(ScrollViewerOffsetMediator),
new PropertyMetadata(OnScrollableWidthMultiplierChanged));
public static void OnScrollableWidthMultiplierChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
var mediator = (ScrollViewerOffsetMediator)o;
var scrollViewer = mediator.ScrollViewer;
if (null != scrollViewer)
{
scrollViewer.ScrollToHorizontalOffset((double)(e.NewValue) * scrollViewer.ScrollableWidth);
}
}
}

Related

Removing or setting an absolute transform of XAML Element

I have a need to have a XAML object always be scaled 1:1, or atleast the imagebrush content, even though it's parent is in a viewbox and the content is compressed in the X direction.
An example: Viewbox contains Label & ImageBrush. I'd like the label text to scale, but only the ImageBrush size - when zoomed out it would only display the top corner of the content.
The viewmodel for the object does not have access to the scale factor. I've been looking for a way to remove or reset the viewbox transform, but I've been unable to find one. Is there one or will I have to propagate the current scale factor from the parent to the end viewmodel? I'd rather not mix presentation logic there unless I absolutely must.
Here's the current XAML I've got so far:
<ImageBrush ImageSource="{Binding Paint, Mode=OneWay, Converter={StaticResource BitmapToImageSourceConverter}}">
<ImageBrush.RelativeTransform>
<MatrixTransform>
<MatrixTransform.Matrix>
<MultiBinding Converter="{StaticResource TimelineMatrixConverter}">
<Binding />
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type MatrixTransform}}" Path="Matrix" />
</MultiBinding>
</MatrixTransform.Matrix>
</MatrixTransform>
</ImageBrush.RelativeTransform>
</ImageBrush>
Here is a basic exemple of what you can do. First extend the Viewbox class:
public class ViewboxEx : Viewbox
{
private FrameworkElement _child;
#region InvertScaleH
/// <summary>
/// InvertScaleH Read-Only Dependency Property
/// </summary>
private static readonly DependencyPropertyKey InvertScaleHPropertyKey
= DependencyProperty.RegisterReadOnly("InvertScaleH", typeof(double), typeof(ViewboxEx),
new FrameworkPropertyMetadata((double)1));
public static readonly DependencyProperty InvertScaleHProperty
= InvertScaleHPropertyKey.DependencyProperty;
/// <summary>
/// Gets the InvertScaleH property. This dependency property
/// indicates invert scale factor to compensate for horizontal scale fo the Viewbox.
/// </summary>
public double InvertScaleH
{
get { return (double)GetValue(InvertScaleHProperty); }
}
/// <summary>
/// Provides a secure method for setting the InvertScaleH property.
/// This dependency property indicates invert scale factor to compensate for horizontal scale fo the Viewbox.
/// </summary>
/// <param name="value">The new value for the property.</param>
protected void SetInvertScaleH(double value)
{
SetValue(InvertScaleHPropertyKey, value);
}
#endregion
#region InvertScaleV
/// <summary>
/// InvertScaleV Read-Only Dependency Property
/// </summary>
private static readonly DependencyPropertyKey InvertScaleVPropertyKey
= DependencyProperty.RegisterReadOnly("InvertScaleV", typeof(double), typeof(ViewboxEx),
new FrameworkPropertyMetadata((double)1));
public static readonly DependencyProperty InvertScaleVProperty
= InvertScaleVPropertyKey.DependencyProperty;
/// <summary>
/// Gets the InvertScaleV property. This dependency property
/// indicates invert scale factor to compensate for vertical scale fo the Viewbox.
/// </summary>
public double InvertScaleV
{
get { return (double)GetValue(InvertScaleVProperty); }
}
/// <summary>
/// Provides a secure method for setting the InvertScaleV property.
/// This dependency property indicates invert scale factor to compensate for vertical scale fo the Viewbox.
/// </summary>
/// <param name="value">The new value for the property.</param>
protected void SetInvertScaleV(double value)
{
SetValue(InvertScaleVPropertyKey, value);
}
#endregion
public ViewboxEx()
{
Loaded += OnLoaded;
SizeChanged += (_,__) => UpdateScale();
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
UpdateChild();
}
protected override void OnVisualChildrenChanged(DependencyObject visualAdded, DependencyObject visualRemoved)
{
base.OnVisualChildrenChanged(visualAdded, visualRemoved);
UpdateChild();
}
private void UpdateChild()
{
if (_child != null)
{
_child.SizeChanged -= OnChild_SizeChanged;
}
_child = Child as FrameworkElement;
if (_child != null)
{
_child.SizeChanged += OnChild_SizeChanged;
}
UpdateScale();
}
private void OnChild_SizeChanged(object sender, SizeChangedEventArgs e)
{
UpdateScale();
}
private void UpdateScale()
{
if (_child == null)
return;
SetInvertScaleH(_child.ActualWidth / ActualWidth);
SetInvertScaleV(_child.ActualHeight / ActualHeight);
}
}
Basically what it does is to listen for the changes of the size of the Viewbox and its content and then calculate the invert scale factor. Now to use this scale factor:
<Window x:Class="YourNamespace.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:YourNamespace"
Title="MainWindow" Height="450" Width="800">
<Grid>
<local:ViewboxEx>
<Grid Width="100" Height="100">
<Ellipse Fill="Green"/>
<TextBlock Text="123" Margin="10,10,0,0">
<TextBlock.RenderTransform>
<ScaleTransform
ScaleX="{Binding InvertScaleH, RelativeSource={RelativeSource AncestorType={x:Type local:ViewboxEx}}}"
ScaleY="{Binding InvertScaleV, RelativeSource={RelativeSource AncestorType={x:Type local:ViewboxEx}}}"/>
</TextBlock.RenderTransform>
</TextBlock>
</Grid>
</local:ViewboxEx>
</Grid>
</Window>
In this exemple the Ellipse follows the Viewbox size change as usual but the text keeps its size.
This is a basic implementation and may not work in all cases.

Altering border's corner radius on WPF Expander control?

Very high up the visual tree in WPF's Expander control is a border element (see screenshot). By default this has a CornerRadius of 3. Is it possible to modify this value?
I'll leave marking as answer for now but I managed to implement the solution as follows:
Using stylesnooper I obtained the style / control template used for the 'standard' Expander control.
Then after discovering it didn't quite behave as expected, figured out that the line <ToggleButton IsChecked="False" ... is wrong and should actually be <ToggleButton IsChecked="{Binding IsExpanded, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"...
Everything then worked as expected.
I made a behavior which modifies the first found border in the ControlTemplate. You can easily extend the behavior with new properties where u want to modify
/// <summary>
/// modifies the first found <see cref="Border"/> in the <see cref="ControlTemplate"/> of the attached <see cref="Control"/>
/// </summary>
public class ModifyBorderBehavior : Behavior<Control>
{
// ##############################################################################################################################
// Properties
// ##############################################################################################################################
#region Properties
/// <summary>
/// The new corner radius
/// </summary>
public CornerRadius CornerRadius
{
get => (CornerRadius)GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
/// <summary>
/// The <see cref="CornerRadius"/> DependencyProperty.
/// </summary>
public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register("CornerRadius", typeof(CornerRadius), typeof(ModifyBorderBehavior));
#endregion
// ##############################################################################################################################
// Constructor
// ##############################################################################################################################
#region Constructor
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.Loaded += _OnLoaded;
}
private void _OnLoaded(object sender, RoutedEventArgs e)
{
//var children = VisualTree.GetVisualChildCollection<Border>(sender);
if (sender is Control control)
{
Border border = VisualTree.GetVisualChild<Border>(control);
if(ReadLocalValue(CornerRadiusProperty) != DependencyProperty.UnsetValue)
border.CornerRadius = CornerRadius;
}
}
#endregion
}
<Expander>
<i:Interaction.Behaviors>
<zls:ModifyBorderBehavior CornerRadius="0"/>
</i:Interaction.Behaviors>
</Expander>

Microsoft WPF Ribbon October 2010 - Bad Image Quality

WPF Ribbon has poor image quality. I added
<Window.Resources>
<Style TargetType="{x:Type Image}">
<Setter Property="RenderOptions.BitmapScalingMode" Value="HighQuality" />
</Style>
</Window.Resources>
in my ribbon windows - but it doesnt helped. Also tried with
<ribbon:RibbonWindow.Resources>
image quality is still bad :-(
Does anybody know a workaround for this problem? My images are 48x48 png and i used them for large image icons
Thanks
Michael
Have you checked out the following page from Microsoft with specific recommendations on image size / DPI?
http://msdn.microsoft.com/en-us/library/windows/desktop/dd316921(v=vs.85).aspx
The reason for you poor quality, is because of incorrect image size. Small icons must be 16 x 16. And large icons must 32 x 32. If the image size is of another size it will be stretched. And the stretching will result in a reduced image quality.
I had the same problem and I created my own usercontrol to solve this.
This is how I did it:
<ribbon:Ribbon>
<ribbon:RibbonTab Header="File">
<ribbon:RibbonGroup Header="File">
<views:ImageButton Command="{Binding LoadCommand}" Caption="Open" SourceImage="/Images/save.png"/>
</ribbon:RibbonGroup>
</ribbon:RibbonTab>
</ribbon:Ribbon>
The image button usercontrol ImageButton.xaml
<UserControl Name="control"
<Button Command="{Binding Command, ElementName=control}" Style="{x:Null}">
<StackPanel>
<infrastructure:AutoGreyableImage Width="32" Source="{Binding SourceImage, ElementName=control}"/>
<TextBlock Text="{Binding Caption, ElementName=control}"/>
</StackPanel>
</Button>
The image button usercontrol ImageButton.xaml.cs
public partial class ImageButton : UserControl
{
public static DependencyProperty CommandProperty = DependencyProperty.Register(
"Command", typeof(ICommand), typeof(ImageButton));
public static DependencyProperty SourceProperty = DependencyProperty.Register(
"SourceImage", typeof(string), typeof(ImageButton));
public static DependencyProperty CaptionProperty = DependencyProperty.Register(
"Caption", typeof(string), typeof(ImageButton));
public ICommand Command
{
get
{
return (ICommand)GetValue(CommandProperty);
}
set
{
SetValue(CommandProperty, value);
}
}
public string SourceImage
{
get
{
return (string)GetValue(SourceProperty);
}
set
{
SetValue(SourceProperty, value);
}
}
public string Caption
{
get
{
return (string)GetValue(CaptionProperty);
}
set
{
SetValue(CaptionProperty, value);
}
}
public ImageButton()
{
InitializeComponent();
}
}
For the AutogreyableImage, I used this post
This is a copy paste of the class
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
/// <summary>
/// Class used to have an image that is able to be gray when the control is not enabled.
/// Author: Thomas LEBRUN (http://blogs.developpeur.org/tom)
/// </summary>
public class AutoGreyableImage : Image
{
/// <summary>
/// Initializes a new instance of the <see cref="AutoGreyableImage"/> class.
/// </summary>
static AutoGreyableImage()
{
// Override the metadata of the IsEnabled property.
IsEnabledProperty.OverrideMetadata(typeof(AutoGreyableImage), new FrameworkPropertyMetadata(true, new PropertyChangedCallback(OnAutoGreyScaleImageIsEnabledPropertyChanged)));
}
/// <summary>
/// Called when [auto grey scale image is enabled property changed].
/// </summary>
/// <param name="source">The source.</param>
/// <param name="args">The <see cref="System.Windows.DependencyPropertyChangedEventArgs"/> instance containing the event data.</param>
private static void OnAutoGreyScaleImageIsEnabledPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs args)
{
var autoGreyScaleImg = source as AutoGreyableImage;
var isEnable = Convert.ToBoolean(args.NewValue);
if (autoGreyScaleImg != null)
{
if (!isEnable)
{
// Get the source bitmap
var bitmapImage = new BitmapImage(new Uri(autoGreyScaleImg.Source.ToString()));
// Convert it to Gray
autoGreyScaleImg.Source = new FormatConvertedBitmap(bitmapImage, PixelFormats.Gray32Float, null, 0);
// Create Opacity Mask for greyscale image as FormatConvertedBitmap does not keep transparency info
autoGreyScaleImg.OpacityMask = new ImageBrush(bitmapImage);
}
else
{
// Set the Source property to the original value.
autoGreyScaleImg.Source = ((FormatConvertedBitmap)autoGreyScaleImg.Source).Source;
// Reset the Opcity Mask
autoGreyScaleImg.OpacityMask = null;
}
}
}
}
I hope this will help you and the others coming

WPF ContextMenu.ItemsSource not resolving from binding

I have the following XAML on a ToolBar:
<emsprim:SplitButton Mode="Split">
<emsprim:SplitButton.Content>
<Image Source="images/16x16/Full Extent 1.png" />
</emsprim:SplitButton.Content>
<emsprim:SplitButton.ContextMenu>
<ContextMenu ItemsSource="{Binding CommandGroups[ZoomToDefinedExtentsCmds]}">
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Command" Value="{Binding Command}" />
<Setter Property="CommandParameter" Value="{Binding ViewID}" />
<Setter Property="Header" Value="{Binding Name}" />
<Setter Property="Icon" Value="{Binding Icon}" />
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
</emsprim:SplitButton.ContextMenu>
</emsprim:SplitButton>
where CommandGroups[ZoomToDefinedExtentsCmds] is an IEnumerable of CommandViewModels. Problem is, when I click on the button, I do not see the list of menu items. However, if I bind the same Datacontext to a Menu, like this:
<MenuItem ItemsSource="{Binding CommandGroups[ZoomToDefinedExtentsCmds]}"
Header="Zoom To"
Margin="5,1,5,0" >
<MenuItem.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Command" Value="{Binding Command}" />
<Setter Property="CommandParameter" Value="{Binding CommandParameter}" />
<Setter Property="Header" Value="{Binding Name}" />
<Setter Property="Icon" Value="{Binding Icon}" />
</Style>
</MenuItem.ItemContainerStyle>
</MenuItem>
I do get the list of MenuItems. Any ideas on what is going on here as there is no binding error in the output VS window. BTW, code for SplitButton is listed below:
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Markup;
using System.Diagnostics;
namespace Controls.Dictionary.Primitives
{
/// <summary>
/// Implemetation of a Split Button
/// </summary>
[TemplatePart(Name = "PART_DropDown", Type = typeof(Button))]
[ContentProperty("Items")]
[DefaultProperty("Items")]
public class SplitButton : Button
{
// AddOwner Dependency properties
public static readonly DependencyProperty PlacementProperty;
public static readonly DependencyProperty PlacementRectangleProperty;
public static readonly DependencyProperty HorizontalOffsetProperty;
public static readonly DependencyProperty VerticalOffsetProperty;
/// <summary>
/// Static Constructor
/// </summary>
static SplitButton()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(SplitButton), new FrameworkPropertyMetadata(typeof(SplitButton)));
// AddOwner properties from the ContextMenuService class, we need callbacks from these properties
// to update the Buttons ContextMenu properties
PlacementProperty = ContextMenuService.PlacementProperty.AddOwner(typeof(SplitButton), new FrameworkPropertyMetadata(PlacementMode.MousePoint, OnPlacementChanged));
PlacementRectangleProperty = ContextMenuService.PlacementRectangleProperty.AddOwner(typeof(SplitButton), new FrameworkPropertyMetadata(Rect.Empty, OnPlacementRectangleChanged));
HorizontalOffsetProperty = ContextMenuService.HorizontalOffsetProperty.AddOwner(typeof(SplitButton), new FrameworkPropertyMetadata(0.0, OnHorizontalOffsetChanged));
VerticalOffsetProperty = ContextMenuService.VerticalOffsetProperty.AddOwner(typeof(SplitButton), new FrameworkPropertyMetadata(0.0, OnVerticalOffsetChanged));
}
/*
* Properties
*
*/
/// <summary>
/// The Split Button's Items property maps to the base classes ContextMenu.Items property
/// </summary>
public ItemCollection Items
{
get
{
EnsureContextMenuIsValid();
return this.ContextMenu.Items;
}
}
/*
* Dependancy Properties & Callbacks
*
*/
/// <summary>
/// Placement of the Context menu
/// </summary>
public PlacementMode Placement
{
get { return (PlacementMode)GetValue(PlacementProperty); }
set { SetValue(PlacementProperty, value); }
}
/// <summary>
/// Placement Property changed callback, pass the value through to the buttons context menu
/// </summary>
private static void OnPlacementChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
SplitButton s = d as SplitButton;
if (s == null) return;
s.EnsureContextMenuIsValid();
s.ContextMenu.Placement = (PlacementMode)e.NewValue;
}
/// <summary>
/// PlacementRectangle of the Context menu
/// </summary>
public Rect PlacementRectangle
{
get { return (Rect)GetValue(PlacementRectangleProperty); }
set { SetValue(PlacementRectangleProperty, value); }
}
/// <summary>
/// PlacementRectangle Property changed callback, pass the value through to the buttons context menu
/// </summary>
private static void OnPlacementRectangleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
SplitButton s = d as SplitButton;
if (s == null) return;
s.EnsureContextMenuIsValid();
s.ContextMenu.PlacementRectangle = (Rect)e.NewValue;
}
/// <summary>
/// HorizontalOffset of the Context menu
/// </summary>
public double HorizontalOffset
{
get { return (double)GetValue(HorizontalOffsetProperty); }
set { SetValue(HorizontalOffsetProperty, value); }
}
/// <summary>
/// HorizontalOffset Property changed callback, pass the value through to the buttons context menu
/// </summary>
private static void OnHorizontalOffsetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
SplitButton s = d as SplitButton;
if (s == null) return;
s.EnsureContextMenuIsValid();
s.ContextMenu.HorizontalOffset = (double)e.NewValue;
}
/// <summary>
/// VerticalOffset of the Context menu
/// </summary>
public double VerticalOffset
{
get { return (double)GetValue(VerticalOffsetProperty); }
set { SetValue(VerticalOffsetProperty, value); }
}
/// <summary>
/// VerticalOffset Property changed callback, pass the value through to the buttons context menu
/// </summary>
private static void OnVerticalOffsetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
SplitButton s = d as SplitButton;
if (s == null) return;
s.EnsureContextMenuIsValid();
s.ContextMenu.VerticalOffset = (double)e.NewValue;
}
/// <summary>
/// Defines the Mode of operation of the Button
/// </summary>
/// <remarks>
/// The SplitButton two Modes are
/// Split (default), - the button has two parts, a normal button and a dropdown which exposes the ContextMenu
/// Dropdown - the button acts like a combobox, clicking anywhere on the button opens the Context Menu
/// </remarks>
public SplitButtonMode Mode
{
get { return (SplitButtonMode)GetValue(ModeProperty); }
set { SetValue(ModeProperty, value); }
}
public static readonly DependencyProperty ModeProperty = DependencyProperty.Register("Mode", typeof(SplitButtonMode), typeof(SplitButton), new FrameworkPropertyMetadata(SplitButtonMode.Split));
/*
* Methods
*
*/
/// <summary>
/// OnApplyTemplate override, set up the click event for the dropdown if present in the template
/// </summary>
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
// set up the event handlers
ButtonBase dropDown = this.Template.FindName("PART_DropDown", this) as ButtonBase;
if (dropDown != null)
dropDown.Click += DoDropdownClick;
}
/// <summary>
/// Make sure the Context menu is not null
/// </summary>
private void EnsureContextMenuIsValid()
{
if (ContextMenu == null)
ContextMenu = new ContextMenu();
}
/*
* Events
*
*/
/// <summary>
/// Event Handler for the Drop Down Button's Click event
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void DoDropdownClick(object sender, RoutedEventArgs e)
{
if (Mode == SplitButtonMode.Dropdown)
return;
if (ContextMenu == null || ContextMenu.HasItems == false) return;
ContextMenu.PlacementTarget = this;
ContextMenu.IsOpen = true;
e.Handled = true;
}
}
}
Problem solved by explicitly setting ContextMenu's DataContext.
ContextMenu is not part of visual tree, therefore, does not resolve DataContext of its "parent'- is one gotcha that gets me every time.
The MenuItem object in your second code snippet, is that outside the SplitButton scope? As in, a direct child of the object container that has the CommandGroups property defined?
I ask because the ContextMenu in the first snippet will have a null DataContext, and as such will not be able to see the CommandGroups property.
I had a similar problem about a year ago, unfortunately, the only way I could solve this was to define the ContextMenu in code and inside the Execute method for a Command. Which enabled me to assign the ItemsSource in code.
To debug DataContext (and other Binding related problems like this) you should create yourself a DebugConverter like:
public class DebugConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
This will help you debug a troublesome binding issue by creating the Binding like: {Binding Converter={StaticResource debugConverter}} and by setting a break point on the return value; line.

Animating Grid Column or Grid Row in XAML?

Is there any way I can animate Grid column width or Grid row height from XAML?
How about a work around? Why not place a grid(or any other desired control) inside the particular row that you want to animate, set the row height to "Auto", then animate the height of the control. It worked for me.
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Button x:Name="ExpandCollapseBtn" Width="100" Click="ExpandCollapse_Click"/>
<WrapPanel x:Name="ToolBox" Grid.Row="1" Height="0">
<Button Content="1" Width="50" Height="50"/>
<Button Content="2" Width="50" Height="50"/>
<Button Content="3" Width="50" Height="50"/>
<Button Content="4" Width="50" Height="50"/>
</WrapPanel>
</Grid>
Code behind:
private bool Expanded = false;
void ExpandCollapse_Click(object sender, RoutedEventArgs e)
{
if (Expanded)
{
var anim = new DoubleAnimation(0, (Duration)TimeSpan.FromSeconds(0.3));
anim.Completed += (s, _) => Expanded = false;
ToolBox.BeginAnimation(ContentControl.HeightProperty, anim);
}
else
{
var anim = new DoubleAnimation(100, (Duration)TimeSpan.FromSeconds(0.3));
anim.Completed += (s, _) => Expanded = true;
ToolBox.BeginAnimation(ContentControl.HeightProperty, anim);
}
}
I admit its not what you are looking for. But its a quick solution(Assuming of course that ultimately you want the UIElement placed inside the grid animated by animating the grid row). You can similarly do it for column width.
The ColumnDefinition.Width and RowDefinition.Height properties are of type GridLength, and there is no built-in animations for this type. So if you want to do that, you will probably have to create your own GridLengthAnimation class. That's probably not too impossible if you take DoubleAnimation as an example, but not easy either...
EDIT: actually, there are several interesting results if you search "GridLength animation" on Google...
http://windowsclient.net/learn/video.aspx?v=70654
http://marlongrech.wordpress.com/2007/08/20/gridlength-animation/
http://www.codeproject.com/KB/WPF/GridLengthAnimation.aspx
I got tired of having to fiddle with XAML to animate grid rows and columns a while ago so I wrote a couple of methods to do it totally from code.
With these you can expand/shrink columns and rows from code with one line:
Animation.AnimationHelper.AnimateGridColumnExpandCollapse(LeftColumn, true, expandedHeight, currentWidth, LeftColumn.MinWidth, 0, 200);
One important thing to note is setting the animation to null on completion. If you don't do this, the grid is still under control of the animation when the animation is complete. This might be fine if the grid doesn't have a splitter, but if the grid has a splitter and you want to be able to resize it manually after the animation completes, then you have to set the animation to null after it completes.
Here are the methods:
/// <summary>
/// Animate expand/collapse of a grid column.
/// </summary>
/// <param name="gridColumn">The grid column to expand/collapse.</param>
/// <param name="expandedWidth">The expanded width.</param>
/// <param name="milliseconds">The milliseconds component of the duration.</param>
/// <param name="collapsedWidth">The width when collapsed.</param>
/// <param name="minWidth">The minimum width of the column.</param>
/// <param name="seconds">The seconds component of the duration.</param>
/// <param name="expand">If true, expand, otherwise collapse.</param>
public static void AnimateGridColumnExpandCollapse(ColumnDefinition gridColumn, bool expand, double expandedWidth, double collapsedWidth,
double minWidth, int seconds, int milliseconds)
{
if( expand && gridColumn.ActualWidth >= expandedWidth)
// It's as wide as it needs to be.
return;
if (!expand && gridColumn.ActualWidth == collapsedWidth)
// It's already collapsed.
return;
Storyboard storyBoard = new Storyboard();
GridLengthAnimation animation = new GridLengthAnimation();
animation.From = new GridLength(gridColumn.ActualWidth);
animation.To = new GridLength(expand ? expandedWidth : collapsedWidth);
animation.Duration = new TimeSpan(0, 0, 0, seconds, milliseconds);
// Set delegate that will fire on completion.
animation.Completed += delegate
{
// Set the animation to null on completion. This allows the grid to be resized manually
gridColumn.BeginAnimation(ColumnDefinition.WidthProperty, null);
// Set the final value manually.
gridColumn.Width = new GridLength(expand ? expandedWidth : collapsedWidth);
// Set the minimum width.
gridColumn.MinWidth = minWidth;
};
storyBoard.Children.Add(animation);
Storyboard.SetTarget(animation, gridColumn);
Storyboard.SetTargetProperty(animation, new PropertyPath(ColumnDefinition.WidthProperty));
storyBoard.Children.Add(animation);
// Begin the animation.
storyBoard.Begin();
}
/// <summary>
/// Animate expand/collapse of a grid row.
/// </summary>
/// <param name="gridRow">The grid row to expand/collapse.</param>
/// <param name="expandedHeight">The expanded height.</param>
/// <param name="collapsedHeight">The collapesed height.</param>
/// <param name="minHeight">The minimum height.</param>
/// <param name="milliseconds">The milliseconds component of the duration.</param>
/// <param name="seconds">The seconds component of the duration.</param>
/// <param name="expand">If true, expand, otherwise collapse.</param>
public static void AnimateGridRowExpandCollapse(RowDefinition gridRow, bool expand, double expandedHeight, double collapsedHeight, double minHeight, int seconds, int milliseconds)
{
if (expand && gridRow.ActualHeight >= expandedHeight)
// It's as high as it needs to be.
return;
if (!expand && gridRow.ActualHeight == collapsedHeight)
// It's already collapsed.
return;
Storyboard storyBoard = new Storyboard();
GridLengthAnimation animation = new GridLengthAnimation();
animation.From = new GridLength(gridRow.ActualHeight);
animation.To = new GridLength(expand ? expandedHeight : collapsedHeight);
animation.Duration = new TimeSpan(0, 0, 0, seconds, milliseconds);
// Set delegate that will fire on completioon.
animation.Completed += delegate
{
// Set the animation to null on completion. This allows the grid to be resized manually
gridRow.BeginAnimation(RowDefinition.HeightProperty, null);
// Set the final height.
gridRow.Height = new GridLength(expand ? expandedHeight : collapsedHeight);
// Set the minimum height.
gridRow.MinHeight = minHeight;
};
storyBoard.Children.Add(animation);
Storyboard.SetTarget(animation, gridRow);
Storyboard.SetTargetProperty(animation, new PropertyPath(RowDefinition.HeightProperty));
storyBoard.Children.Add(animation);
// Begin the animation.
storyBoard.Begin();
}
I built upon the AnimationHelper class provided by Nigel Shaw and wrapped it in a reusable GridAnimationBehavior which can be attached to the RowDefinition and ColumnDefinition elements.
/// <summary>
/// Wraps the functionality provided by the <see cref="AnimationHelper"/> class
/// in a behavior which can be used with the <see cref="ColumnDefinition"/>
/// and <see cref="RowDefinition"/> types.
/// </summary>
public class GridAnimationBehavior : DependencyObject
{
#region Attached IsExpanded DependencyProperty
/// <summary>
/// Register the "IsExpanded" attached property and the "OnIsExpanded" callback
/// </summary>
public static readonly DependencyProperty IsExpandedProperty =
DependencyProperty.RegisterAttached("IsExpanded", typeof(bool), typeof(GridAnimationBehavior),
new FrameworkPropertyMetadata(OnIsExpandedChanged));
public static void SetIsExpanded(DependencyObject dependencyObject, bool value)
{
dependencyObject.SetValue(IsExpandedProperty, value);
}
#endregion
#region Attached Duration DependencyProperty
/// <summary>
/// Register the "Duration" attached property
/// </summary>
public static readonly DependencyProperty DurationProperty =
DependencyProperty.RegisterAttached("Duration", typeof(TimeSpan), typeof(GridAnimationBehavior),
new FrameworkPropertyMetadata(TimeSpan.FromMilliseconds(200)));
public static void SetDuration(DependencyObject dependencyObject, TimeSpan value)
{
dependencyObject.SetValue(DurationProperty, value);
}
private static TimeSpan GetDuration(DependencyObject dependencyObject)
{
return (TimeSpan)dependencyObject.GetValue(DurationProperty);
}
#endregion
#region GridCellSize DependencyProperty
/// <summary>
/// Use a private "GridCellSize" dependency property as a temporary backing
/// store for the last expanded grid cell size (row height or column width).
/// </summary>
private static readonly DependencyProperty GridCellSizeProperty =
DependencyProperty.Register("GridCellSize", typeof(double), typeof(GridAnimationBehavior),
new UIPropertyMetadata(0.0));
private static void SetGridCellSize(DependencyObject dependencyObject, double value)
{
dependencyObject.SetValue(GridCellSizeProperty, value);
}
private static double GetGridCellSize(DependencyObject dependencyObject)
{
return (double)dependencyObject.GetValue(GridCellSizeProperty);
}
#endregion
/// <summary>
/// Called when the attached <c>IsExpanded</c> property changed.
/// </summary>
private static void OnIsExpandedChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
var duration = GetDuration(dependencyObject);
var rowDefinition = dependencyObject as RowDefinition;
if (rowDefinition != null)
{
// The IsExpanded attached property of a RowDefinition changed
if ((bool)e.NewValue)
{
var expandedHeight = GetGridCellSize(rowDefinition);
if (expandedHeight > 0)
{
// Animate row height back to saved expanded height.
AnimationHelper.AnimateGridRowExpandCollapse(rowDefinition, true, expandedHeight, rowDefinition.ActualHeight, 0, duration);
}
}
else
{
// Save expanded height and animate row height down to zero.
SetGridCellSize(rowDefinition, rowDefinition.ActualHeight);
AnimationHelper.AnimateGridRowExpandCollapse(rowDefinition, false, rowDefinition.ActualHeight, 0, 0, duration);
}
}
var columnDefinition = dependencyObject as ColumnDefinition;
if (columnDefinition != null)
{
// The IsExpanded attached property of a ColumnDefinition changed
if ((bool)e.NewValue)
{
var expandedWidth = GetGridCellSize(columnDefinition);
if (expandedWidth > 0)
{
// Animate column width back to saved expanded width.
AnimationHelper.AnimateGridColumnExpandCollapse(columnDefinition, true, expandedWidth, columnDefinition.ActualWidth, 0, duration);
}
}
else
{
// Save expanded width and animate column width down to zero.
SetGridCellSize(columnDefinition, columnDefinition.ActualWidth);
AnimationHelper.AnimateGridColumnExpandCollapse(columnDefinition, false, columnDefinition.ActualWidth, 0, 0, duration);
}
}
}
}
Note that I tweaked Nigel's code a bit to use a parameter of type TimeSpan for the animation duration instead of separate seconds and milliseconds parameters.
This behavior makes the animation of grid rows/columns MVVM friendly (XAML-only, no code behind required). Example:
<Grid.RowDefinitions>
<RowDefinition Height="*" Behaviors:GridAnimationBehavior.IsExpanded="{Binding IsUpperPaneVisible}" />
<RowDefinition Height="*" />
<RowDefinition Height="*" Behaviors:GridAnimationBehavior.IsExpanded="{Binding IsLowerPaneVisible}" />
</Grid.RowDefinitions>
I added this answer because the original poster asked for a pure XAML solution.
The MahApps.Metro library has a built-in control for this. The source can be found here.
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="48" x:Name="HamburgerMenuColumn" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.Resources>
<Storyboard x:Key="CloseMenu" Storyboard.TargetName="HamburgerMenuColumn" Storyboard.TargetProperty="(ColumnDefinition.Width)">
<metro:GridLengthAnimation To="48" Duration="00:00:00"></metro:GridLengthAnimation>
</Storyboard>
</Grid.Resources>
</Grid>
This work for me
In XAML:
<Grid >
<Grid.RenderTransform>
<TranslateTransform />
</Grid.RenderTransform>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition x:Name="SecondRow" Height="100"/>
</Grid.RowDefinitions>
</Grid >
Code Behind:
if (SecondRow.Height == new GridLength(0))
{
Task.Run(() => {
for(int i = 0; i <= 20; i++)
{
this.Dispatcher.Invoke((Action)delegate { SecondRow.Height = new GridLength(5*i); });
Task.Delay(1).Wait();
}
});
}
else
{
Task.Run(() => {
for (int i = 20; i >= 0; i--)
{
this.Dispatcher.Invoke((Action)delegate { SecondRow.Height = new GridLength(5 * i); });
Task.Delay(1).Wait();
}
});
}

Resources