Removing or setting an absolute transform of XAML Element - wpf

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.

Related

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>

How can I print a collection of UserControls in WPF?

Say I have the following small UserControl:
<StackPanel>
<TextBlock Text="{Binding Title, StringFormat=Name: {0}}" FontWeight="Bold"/>
<Separator/>
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=. ,StringFormat=Detail: {0}}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Line Stroke="Black" HorizontalAlignment="Stretch" Margin="0,10"
X2="{Binding ActualWidth, RelativeSource={RelativeSource Self}}"
StrokeDashArray="2 2" StrokeThickness="1" />
</StackPanel>
My application will generate a collection of these controls and then they should be printed with automatic paginating. The number of items that is shown in the ItemsControl is variable. I scourged the Net, read the chapter about printing in Pro WPF 4.5 Unleashed, but still I do not see how to accomplish this.
Any guidance is very welcome.
EDIT: any guidance is welcome. As I have the data available in a view model it will not be too difficult to redirect its data elsewhere. But where?
Since you require automatic pagination, you'll need to create a DocumentPaginator object.
The following is stolen from this example and modified for your case:
/// <summary>
/// Document paginator.
/// </summary>
public class UserControlDocumentPaginator : DocumentPaginator
{
private readonly UserControlItem[] userControlItems;
private Size pageSize;
private int pageCount;
private int maxRowsPerPage;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="userControlItems">The list of userControl items to display.</param>
/// <param name="pageSize">The size of the page in pixels.</param>
public UserControlDocumentPaginator
(
UserControlItem[] userControlItems,
Size pageSize
)
{
this.userControlItems = userControlItems;
this.pageSize = pageSize;
PaginateUserControlItems();
}
/// <summary>
/// Computes the page count based on the number of userControl items
/// and the page size.
/// </summary>
private void PaginateUserControlItems()
{
double actualHeight;
foreach (var uc in userControlItems)
{
actualHeight += uc.ActualHeight;
}
pageCount = (int)Math.Ceiling(actualHeight / pageSize.Height);
}
/// <summary>
/// Gets a range of userControl items from an array.
/// </summary>
/// <param name="array">The userControl items array.</param>
/// <param name="start">Start index.</param>
/// <param name="end">End index.</param>
/// <returns></returns>
private static UserControlItem[] GetRange(UserControlItem[] array, int start, int end)
{
List<UserControlItem> userControlItems = new List<UserControlItem>();
for (int i = start; i < end; i++)
{
if (i >= array.Count())
{
break;
}
userControlItems.Add(array[i]);
}
return userControlItems.ToArray();
}
#region DocumentPaginator Members
/// <summary>
/// When overridden in a derived class, gets the DocumentPage for the
/// specified page number.
/// </summary>
/// <param name="pageNumber">
/// The zero-based page number of the document page that is needed.
/// </param>
/// <returns>
/// The DocumentPage for the specified pageNumber, or DocumentPage.Missing
/// if the page does not exist.
/// </returns>
public override DocumentPage GetPage(int pageNumber)
{
// Compute the range of userControl items to display
int start = pageNumber * maxRowsPerPage;
int end = start + maxRowsPerPage;
UserControlListPage page = new UserControlListPage(GetRange(userControlItems, start, end), pageSize);
page.Measure(pageSize);
page.Arrange(new Rect(pageSize));
return new DocumentPage(page);
}
/// <summary>
/// When overridden in a derived class, gets a value indicating whether
/// PageCount is the total number of pages.
/// </summary>
public override bool IsPageCountValid
{
get { return true; }
}
/// <summary>
/// When overridden in a derived class, gets a count of the number of
/// pages currently formatted.
/// </summary>
public override int PageCount
{
get { return pageCount; }
}
/// <summary>
/// When overridden in a derived class, gets or sets the suggested width
/// and height of each page.
/// </summary>
public override System.Windows.Size PageSize
{
get
{
return pageSize;
}
set
{
if (pageSize.Equals(value) != true)
{
pageSize = value;
PaginateUserControlItems();
}
}
}
/// <summary>
/// When overridden in a derived class, returns the element being paginated.
/// </summary>
public override IDocumentPaginatorSource Source
{
get { return null; }
}
#endregion
}
Then in your code behind for your Window, get a list of your user controls (replace YourUserControlContainer with the name of the container in your Window). Create a button named PrintButton and attach the Click event to the PrintButton_Click method below. Put in code behind where appropriate:
List<UserControl> userControlItems = new List<UserControl>();
// 8.5 x 11 paper
Size pageSize = new Size(816, 1056);
private void PrintButton_Click(object sender, RoutedEventArgs e)
{
userControlItems = YourUserControlContainer.Children.ToList();
UserControlDocumentPaginator paginator = new UserControlDocumentPaginator
(
userControlItems.ToArray(),
pageSize
);
var dialog = new PrintDialog();
if (dialog.ShowDialog() != true) return;
dialog.PrintDocument(paginator, "Custom Paginator Print Job");
}
Edit You're right, I forgot a class. You'll need something like this:
public partial class UserControlListPage : UserControl
{
private readonly UserControlItem[] userControlItems;
private readonly Size pageSize;
public UserControlListPage
(
UserControlItem[] userControlItems,
Size pageSize
)
{
InitializeComponent();
this.userControlItems = userControlItems;
this.pageSize = pageSize;
}
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
Point point = new Point(0, 0);
foreach (UserControlItem item in userControlItems)
{
point.X = 0;
itemImageSource = CopyControlToImageSource(item);
drawingContext.DrawImage(itemImageSource, point);
point.Y += itemImageSource.Height;
}
}
}
Then somewhere put this:
/// <summary>
/// Gets an image "screenshot" of the specified UIElement
/// </summary>
/// <param name="source">UIElement to screenshot</param>
/// <param name="scale" value="1">Scale to render the screenshot</param>
/// <returns>Byte array of BMP data</returns>
private static byte[] GetUIElementSnapshot(UIElement source, double scale = 1)
{
double actualHeight = source.RenderSize.Height;
double actualWidth = source.RenderSize.Width;
double renderHeight = actualHeight * scale;
double renderWidth = actualWidth * scale;
RenderTargetBitmap renderTarget = new RenderTargetBitmap((int)renderWidth, (int)renderHeight, 96, 96, PixelFormats.Pbgra32);
VisualBrush sourceBrush = new VisualBrush(source);
DrawingVisual drawingVisual = new DrawingVisual();
DrawingContext drawingContext = drawingVisual.RenderOpen();
using (drawingContext)
{
drawingContext.PushTransform(new ScaleTransform(scale, scale));
drawingContext.DrawRectangle(sourceBrush, null, new Rect(new System.Windows.Point(0, 0), new System.Windows.Point(actualWidth, actualHeight)));
}
renderTarget.Render(drawingVisual);
Byte[] _imageArray = null;
BmpBitmapEncoder bmpEncoder = new BmpBitmapEncoder();
bmpEncoder.Frames.Add(BitmapFrame.Create(renderTarget));
using (MemoryStream outputStream = new MemoryStream())
{
bmpEncoder.Save(outputStream);
_imageArray = outputStream.ToArray();
}
return _imageArray;
}
public static System.Windows.Media.Imaging.BitmapImage CopyControlToImageSource(UIElement UserControl)
{
ImageConverter ic = new ImageConverter();
System.Drawing.Image img = (System.Drawing.Image)ic.ConvertFrom(GetUIElementSnapshot(UserControl));
MemoryStream ms = new MemoryStream();
img.Save(ms, System.Drawing.Imaging.ImageFormat.Bmp);
System.Windows.Media.Imaging.BitmapImage image = new BitmapImage();
image.BeginInit();
ms.Seek(0, SeekOrigin.Begin);
image.StreamSource = ms;
image.EndInit();
return image;
}
Its a bit difficult to understand where you stuck at exactly since we do not have you code and we do not know about your data, however here is an simple example:
<Window x:Class="WpfApplication1.PrintVisualDemo"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Print Visual Demo" Height="350" Width="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="0.9*" />
<RowDefinition Height="0.2*"/>
</Grid.RowDefinitions>
<StackPanel Name="printPanel"
Grid.Row="0"
Margin="5">
<Label Content="Sweet Baby"
HorizontalAlignment="Center"
FontSize="40"
FontFamily="Calibri">
<Label.Foreground>
<LinearGradientBrush Opacity="1"
StartPoint="0,0.5" EndPoint="1,0.5">
<LinearGradientBrush.GradientStops>
<GradientStop Color="Blue" Offset="0" />
<GradientStop Color="Red" Offset="0.5" />
<GradientStop Color="Green" Offset="1" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Label.Foreground>
</Label>
<Image Source="D:\Temp\baby.jpg"
Height="150"
Width="200">
</Image>
</StackPanel>
<Button Content="Print"
Grid.Row="1" Margin="5"
Height="40" Width="150"
HorizontalAlignment="Center"
VerticalAlignment="Center" Click="Button_Click" />
</Grid>
</Window>
private void Button_Click(object sender, RoutedEventArgs e)
{
PrintDialog pd = new PrintDialog();
if (pd.ShowDialog() != true) return;
pd.PrintVisual(printPanel, "Printing StackPanel...");
}
PrintVisual method should do the job.
If you want to scale your visual to fit the page you could follow those steps:
PrintDialog printDlg = new System.Windows.Controls.PrintDialog();
if (printDlg.ShowDialog() == true)
{
//get selected printer capabilities
System.Printing.PrintCapabilities capabilities = printDlg.PrintQueue.GetPrintCapabilities(printDlg.PrintTicket);
//get scale of the print wrt to screen of WPF visual
double scale = Math.Min(capabilities.PageImageableArea.ExtentWidth / this.ActualWidth, capabilities.PageImageableArea.ExtentHeight /
this.ActualHeight);
//Transform the Visual to scale
this.LayoutTransform = new ScaleTransform(scale, scale);
//get the size of the printer page
Size sz = new Size(capabilities.PageImageableArea.ExtentWidth, capabilities.PageImageableArea.ExtentHeight);
//update the layout of the visual to the printer page size.
this.Measure(sz);
this.Arrange(new Rect(new Point(capabilities.PageImageableArea.OriginWidth, capabilities.PageImageableArea.OriginHeight), sz));
//now print the visual to printer to fit on the one page.
printDlg.PrintVisual(this, "First Fit to Page WPF Print");
}
As you can see all you need to do is remeasure and rearrange the specific element with new size and then you can call PrintVisual method.
I hope this helps you any futher. Consider post is more code or upload your project online somewhere.

How to disable the WPF ScrollViewer to refresh the view

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);
}
}
}

WPF Nested Scrollviewers - giving control back to parent scollviewer

This is what my control tree looks like:
<window>
<scrollviewer>
<expander>
<scrollviewer>
<grid>
</grid>
</scrollviewer>
</expander>
<expander>
<scrollviewer>
<grid>
</grid>
</scrollviewer>
</expander>
</scrollviewer>
</window>
Using the mouse wheel, the control automatically passes from parent to child scrollviewer, but when I scroll to the end of the child scrollviewer the control doesn't pass back to the parent scorllviewer. How do I achieve this?
The expander, grid and the scrollviewers are dynamically generated.
I get a similar trouble in my application. I correct it by a depency property that will catch and pass the event too his parent. This can be applied to any control that have a scroll in it. But for me, i didn't need to validate if it was at the end of the scroll to send to his parent. You will just have to add, in the OnValueChanged method, a validation for if the scroll is at the end or at the top to send to his parent.
using System.Windows.Controls;
public static class SendMouseWheelToParent
{
public static readonly DependencyProperty ScrollProperty
= DependencyProperty.RegisterAttached("IsSendingMouseWheelEventToParent",
typeof(bool),
typeof(SendMouseWheelToParent),
new FrameworkPropertyMetadata(OnValueChanged));
/// <summary>
/// Gets the IsSendingMouseWheelEventToParent for a given <see cref="TextBox"/>.
/// </summary>
/// <param name="control">
/// The <see cref="TextBox"/> whose IsSendingMouseWheelEventToParent is to be retrieved.
/// </param>
/// <returns>
/// The IsSendingMouseWheelEventToParent, or <see langword="null"/>
/// if no IsSendingMouseWheelEventToParent has been set.
/// </returns>
public static bool? GetIsSendingMouseWheelEventToParent(Control control)
{
if (control == null)
throw new ArgumentNullException("");
return control.GetValue(ScrollProperty) as bool?;
}
/// <summary>
/// Sets the IsSendingMouseWheelEventToParent for a given <see cref="TextBox"/>.
/// </summary>
/// <param name="control">
/// The <see cref="TextBox"/> whose IsSendingMouseWheelEventToParent is to be set.
/// </param>
/// <param name="IsSendingMouseWheelEventToParent">
/// The IsSendingMouseWheelEventToParent to set, or <see langword="null"/>
/// to remove any existing IsSendingMouseWheelEventToParent from <paramref name="control"/>.
/// </param>
public static void SetIsSendingMouseWheelEventToParent(Control control, bool? sendToParent)
{
if (control == null)
throw new ArgumentNullException("");
control.SetValue(ScrollProperty, sendToParent);
}
private static void OnValueChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
var scrollViewer = dependencyObject as Control;
bool? IsSendingMouseWheelEventToParent = e.NewValue as bool?;
scrollViewer.PreviewMouseWheel -= scrollViewer_PreviewMouseWheel;
if (IsSendingMouseWheelEventToParent != null && IsSendingMouseWheelEventToParent != false)
{
scrollViewer.SetValue(ScrollProperty, IsSendingMouseWheelEventToParent);
scrollViewer.PreviewMouseWheel += scrollViewer_PreviewMouseWheel;
}
}
private static void scrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
var scrollview = sender as Control;
var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);
eventArg.RoutedEvent = UIElement.MouseWheelEvent;
eventArg.Source = sender;
var parent = scrollview.Parent as UIElement;
parent.RaiseEvent(eventArg);
}
}
Inspired by Janick's answer and some others, I have an implementation that scrolls ancestor ScrollViewers when inner ones (including from ListView, ListBox, DataGrid) scroll past their top/bottom. The code is in my answer here to a very similar question.

UserControl exposing multiple content properties! How exciting that would be!

I am trying to create a UserControl that will hopefully be able to expose multiple content properties. However, I am ridden with failure!
The idea would be to create this great user control (we'll call it MultiContent) that exposes two content properties so that I could do the following:
<local:MultiContent>
<local:MultiContent.ListContent>
<ListBox x:Name="lstListOfStuff" Width="50" Height="50" />
</local:MultiContent.ListContent>
<local:MultiContent.ItemContent>
<TextBox x:Name="txtItemName" Width="50" />
</local:MultiContent.ItemContent>
</local:MultiContent>
This would be very useful, now I can change ListContent and ItemContent depending on the situation, with common functionality factored out into the MultiContent user control.
However, the way I currently have this implemented, I cannot access the UI elements inside of these content properties of the MultiContent control. For instance, lstListOfStuff and txtItemName are both null when I try to access them:
public MainPage() {
InitializeComponent();
this.txtItemName.Text = "Item 1"; // <-- txtItemName is null, so this throws an exception
}
Here is how I have implemented the MultiContent user control:
XAML: MultiContent.xaml
<UserControl x:Class="Example.MultiContent"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="400" Height="300">
<Grid x:Name="LayoutRoot" Background="White">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="100" />
</Grid.ColumnDefinitions>
<ContentControl x:Name="pnlList" Grid.Column="0" />
<ContentControl x:Name="pnlItem" Grid.Column="1" />
</Grid>
</UserControl>
Code Behind: MultiContent.xaml.cs
// Namespaces Removed
namespace Example
{
public partial class MultiContent : UserControl
{
public UIElement ListContent
{
get { return (UIElement)GetValue(ListContentProperty); }
set
{
this.pnlList.Content = value;
SetValue(ListContentProperty, value);
}
}
public static readonly DependencyProperty ListContentProperty =
DependencyProperty.Register("ListContent", typeof(UIElement), typeof(MultiContent), new PropertyMetadata(null));
public UIElement ItemContent
{
get { return (UIElement)GetValue(ItemContentProperty); }
set
{
this.pnlItem.Content = value;
SetValue(ItemContentProperty, value);
}
}
public static readonly DependencyProperty ItemContentProperty =
DependencyProperty.Register("ItemContent", typeof(UIElement), typeof(MultiContent), new PropertyMetadata(null));
public MultiContent()
{
InitializeComponent();
}
}
}
I am probably implementing this completely wrong. Does anyone have any idea how I could get this to work properly? How can I access these UI elements by name from the parent control? Any suggestions on how to do this better? Thanks!
You can definitely achieve your goal but you need to take a different approach.
In your solution you're trying to have a dependency property for of a UIElement - and since it never gets set and default value is null that's why you get a NullReference exception. You could probably go ahead by changing the default value from null to new TextBox or something like that but even if it did work it still would feel like a hack.
In Silverlight you have to implement Dpendency Properties yourself. However you've not implemented them as they should be - I tend to use this dependency property generator to do so.
One of the great things about DPs is that they support change notification. So with that in mind all you have to do to get your sample working is define to DPs: ItemContent and ListContent with the same type as Content (object) and when the framework notifies you that either of them has been changed, simply update your textboxes! So here is the code to do this:
MultiContent.xaml:
<Grid x:Name="LayoutRoot" Background="White">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="100" />
</Grid.ColumnDefinitions>
<ContentControl x:Name="pnlList" Grid.Column="0" />
<ContentControl x:Name="pnlItem" Grid.Column="1" />
</Grid>
MultiContent.xaml.cs:
namespace MultiContent
{
public partial class MultiContent : UserControl
{
#region ListContent
/// <summary>
/// ListContent Dependency Property
/// </summary>
public object ListContent
{
get { return (object)GetValue(ListContentProperty); }
set { SetValue(ListContentProperty, value); }
}
/// <summary>
/// Identifies the ListContent Dependency Property.
/// </summary>
public static readonly DependencyProperty ListContentProperty =
DependencyProperty.Register("ListContent", typeof(object),
typeof(MultiContent), new PropertyMetadata(null, OnListContentPropertyChanged));
private static void OnListContentPropertyChanged
(object sender, DependencyPropertyChangedEventArgs e)
{
MultiContent m = sender as MultiContent;
m.OnPropertyChanged("ListContent");
}
#endregion
#region ItemContent
/// <summary>
/// ItemContent Dependency Property
/// </summary>
public object ItemContent
{
get { return (object)GetValue(ItemContentProperty); }
set { SetValue(ItemContentProperty, value); }
}
/// <summary>
/// Identifies the ItemContent Dependency Property.
/// </summary>
public static readonly DependencyProperty ItemContentProperty =
DependencyProperty.Register("ItemContent", typeof(object),
typeof(MultiContent), new PropertyMetadata(null, OnItemContentPropertyChanged));
private static void OnItemContentPropertyChanged
(object sender, DependencyPropertyChangedEventArgs e)
{
MultiContent m = sender as MultiContent;
m.OnPropertyChanged("ItemContent");
}
#endregion
/// <summary>
/// Event called when any chart property changes
/// Note that this property is not used in the example but is good to have if you plan to extend the class!
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Called to invoke the property changed event
/// </summary>
/// <param name="propertyName">The property that has changed</param>
protected void OnPropertyChanged(string propertyName)
{
if (propertyName == "ListContent")
{
// The ListContent property has been changed, let's update the control!
this.pnlList.Content = this.ListContent;
}
if (propertyName == "ItemContent")
{
// The ListContent property has been changed, let's update the control!
this.pnlItem.Content = this.ItemContent;
}
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public MultiContent()
{
InitializeComponent();
}
}
}
MainPage.xaml:
<Grid x:Name="LayoutRoot" Background="White">
<local:MultiContent>
<local:MultiContent.ListContent>
<ListBox x:Name="lstListOfStuff" Width="50" Height="50" />
</local:MultiContent.ListContent>
<local:MultiContent.ItemContent>
<TextBox x:Name="txtItemName" Width="50" />
</local:MultiContent.ItemContent>
</local:MultiContent>
</Grid>
This should do the trick!

Resources