Zoom into ListView contents without also scaling scroll bars - c#

I have a GridView in a ListView. I want to add a Ctrl+MWheelUp zoom to the contents.
I've achieved the zoom part using a ScaleTransform with the below code, however, as this is applied to the ListView as a whole, it also scales the scroll bars. Ideally, I'd like scrollbars to remain a fixed size (although obviously adjusting to the change in inner-content) - however, I'm not sure how I could achieve this. Would the only way to be to apply the ScaleTransform to every child of every GridViewColumn, or is there another method I could use to apply it to the ListView as a whole, without also scaling the scroll bars?
My (simplified) xaml:
<ListView ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto"
x:Name="listView">
<ListView.View>
<GridView>
<GridViewColumn>...</GridViewColumn>
<GridViewColumn>...</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
xaml.cs:
public Control()
{
InitializeComponent();
var mouseWheelZoom = new MouseWheelZoom(listView);
PreviewMouseWheel += mouseWheelZoom.Zoom;
}
MouseWheelZoom
public class MouseWheelZoom
{
private readonly FrameworkElement _element;
private double _currentZoomFactor;
public MouseWheelZoom(FrameworkElement element)
{
_element = element;
_currentZoomFactor = 1.0;
}
public void Zoom(object sender, MouseWheelEventArgs e)
{
var handle = (Keyboard.Modifiers & ModifierKeys.Control) > 0;
if (!handle)
return;
ApplyZoom(e.Delta);
}
private void ApplyZoom(int delta)
{
var zoomScale = delta / 500.0;
var newZoomFactor = _currentZoomFactor += zoomScale;
_element.LayoutTransform = new ScaleTransform(newZoomFactor, newZoomFactor);
_currentZoomFactor = newZoomFactor;
}
}

I just created an attached property that seems to produce the effect you are looking for. Here is the code for the property.
internal static class ListViewBehaviors
{
public static readonly DependencyProperty ContentTransformProperty = DependencyProperty.RegisterAttached("ContentTransform", typeof(Transform), typeof(ListViewBehaviors),
new PropertyMetadata((Transform)null, OnContentTransformChanged));
public static Transform GetContentTransform(ListView obj)
{
return (Transform)obj.GetValue(ContentTransformProperty);
}
public static void SetContentTransform(ListView obj, Transform value)
{
obj.SetValue(ContentTransformProperty, value);
}
private static void OnContentTransformChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
ListView view = obj as ListView;
if (view != null)
{
if (view.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated)
{
EventHandler handler = null;
handler = (s, a) => ListView_ItemContainerGenerator_StatusChanged(view, handler);
view.ItemContainerGenerator.StatusChanged += handler;
}
else
{
UpdateTransform(view);
}
}
}
private static void ListView_ItemContainerGenerator_StatusChanged(ListView view, EventHandler handler)
{
if (view.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
{
view.ItemContainerGenerator.StatusChanged -= handler;
UpdateTransform(view);
}
}
private static void UpdateTransform(ListView view)
{
if (view.IsArrangeValid)
{
DoUpdateTransform(view);
}
else
{
EventHandler handler = null;
handler = (s, e) => LayoutUpdated(view, handler);
view.LayoutUpdated += handler;
}
}
private static void LayoutUpdated(ListView view, EventHandler handler)
{
view.LayoutUpdated -= handler;
DoUpdateTransform(view);
}
private static void DoUpdateTransform(ListView view)
{
ScrollViewer scroller = VisualTreeUtility.FindDescendant<ScrollViewer>(view);
if (scroller != null)
{
Transform transform = GetContentTransform(view);
FrameworkElement header = VisualTreeUtility.FindDescendant<ScrollViewer>(scroller);
if (header != null)
{
header.LayoutTransform = transform;
}
FrameworkElement content = scroller.Template.FindName("PART_ScrollContentPresenter", scroller) as FrameworkElement;
if (content != null)
{
content.LayoutTransform = transform;
}
}
}
}
Also, here is the code for the VisualTreeUtility.FindDescendant method(s).
public static class VisualTreeUtility
{
public static T FindDescendant<T>(DependencyObject ancestor) where T : DependencyObject
{
return FindDescendant<T>(ancestor, item => true);
}
public static T FindDescendant<T>(DependencyObject ancestor, Predicate<T> predicate) where T : DependencyObject
{
return FindDescendant(typeof(T), ancestor, item => predicate((T)item)) as T;
}
public static DependencyObject FindDescendant(Type itemType, DependencyObject ancestor, Predicate<DependencyObject> predicate)
{
if (itemType == null) throw new ArgumentNullException("itemType");
if (ancestor == null) throw new ArgumentNullException("ancestor");
if (predicate == null) throw new ArgumentNullException("predicate");
if (!typeof(DependencyObject).IsAssignableFrom(itemType)) throw new ArgumentException("itemType", "The passed in type must be or extend DependencyObject");
Queue<DependencyObject> queue = new Queue<DependencyObject>();
queue.Enqueue(ancestor);
while (queue.Count > 0)
{
DependencyObject currentChild = queue.Dequeue();
if (currentChild != ancestor && itemType.IsAssignableFrom(currentChild.GetType()))
{
if(predicate.Invoke(currentChild))
{
return currentChild;
}
}
int count = VisualTreeHelper.GetChildrenCount(currentChild);
for (int i = 0; i < count; ++i)
{
queue.Enqueue(VisualTreeHelper.GetChild(currentChild, i));
}
}
return null;
}
}
And here is how you can use the property:
<ListView
ItemsSource="{Binding Items}">
<local:ListViewBehaviors.ContentTransform>
<ScaleTransform ScaleX="2" ScaleY="2" />
</local:ListViewBehaviors.ContentTransform>
<ListView.View>
...
</ListView.View>
</ListView>
Let me know if anything is missing or confusing.

You could use the always handy FindChild<T> function to retrieve the ScrollContentPresenter inside the ListView, and use your zooming function with it.
public Control()
{
InitializeComponent();
this.Loaded += new RoutedEventHandler(Control_Loaded);
}
private void Control_Loaded(object sender, RoutedEventArgs e)
{
var presenter = FindChild<ScrollContentPresenter>(listView, null);
var mouseWheelZoom = new MouseWheelZoom(presenter);
PreviewMouseWheel += mouseWheelZoom.Zoom;
}
Note that I've put the code inside the Loaded event handler. That's because the ScrollContentPresenter is part of the ListView Template and not a direct part of your view, so it won't exist until the control is fully loaded with its Styles and Templates.
PD.: Also worth noting that some other parts of the ListView, like headers and such, won't be zoomed. Only the items will.

Haven't tried, but you can try to do something like
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel /> <!-- scale this, it's inside ScrollViewer -->
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>

Related

How to pass information from an attached behavior to the viewmodel with static events?

I have an attached property to textboxes in my view. The attached property performs validation on the textbox input and performs other chores. The attached property validation routine raises an event which is being watched by the viewmodel.
Does this "violate" MVVM reasoning by having the viewmodel obtain the invalid TextBoxes?
How will the GC deal with the static events from the attached property when the usercontrol containing the textboxes is removed?
If specific code is needed to avoid memory leaks, how is that done?
Is there a preferred way to do this?
Sorry for the long list, but Google does not address this situation.
Any and all help is appreciated. Thank you for your consideration.
(VS2010 .net 4.5)
TIA
ViewModel
class CheckInViewModel : SimpleViewModelBase
{
public CheckInViewModel()
{
InValidTextBoxes = new List<TextBox>();
Stargate_V.Helpers.ColorMaskingTextBoxBehavior.Validated += (sender, e) =>
{
if (e.valid)
InValidTextBoxes.Remove(e.sender);
else
InValidTextBoxes.Add(e.sender);
};
}
List<TextBox> InValidTextBoxes;
}
XAML
<TextBox
h:ColorMaskingTextBoxBehavior.Mask="^[MmFf]$"
Text="{Binding Sex}"
Height="24" HorizontalAlignment="Right" Margin="0,55,665,0" VerticalAlignment ="Top" Width="36" />
Attached Properity
public class ColorMaskingTextBoxBehavior : DependencyObject
{
// Entrance point from Xaml
public static readonly DependencyProperty MaskProperty = DependencyProperty.RegisterAttached("Mask",
typeof(string),
typeof(ColorMaskingTextBoxBehavior),
new FrameworkPropertyMetadata(OnMaskChanged));
...........................
// Callback from XAML initialization of the attached property.
private static void OnMaskChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
var textBox = dependencyObject as TextBox;
var mask = e.NewValue as string;
textBox.PreviewTextInput -= textBox_PreviewTextInput;
textBox.PreviewKeyDown -= textBox_PreviewKeyDown;
DataObject.RemovePastingHandler(textBox, Pasting);
DataObject.RemoveCopyingHandler(textBox, NoDragCopy);
CommandManager.RemovePreviewExecutedHandler(textBox, NoCutting);
if (mask == null)
{
textBox.ClearValue(MaskProperty);
textBox.ClearValue(MaskExpressionProperty);
}
else
{
textBox.SetValue(MaskProperty, mask);
SetMaskExpression(textBox, new Regex(mask, RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace));
textBox.PreviewTextInput += textBox_PreviewTextInput;
textBox.PreviewKeyDown += textBox_PreviewKeyDown;
DataObject.AddPastingHandler(textBox, Pasting);
DataObject.AddCopyingHandler(textBox, NoDragCopy);
CommandManager.AddPreviewExecutedHandler(textBox, NoCutting);
}
}
private static void textBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
var textBox = sender as TextBox;
var maskExpression = GetMaskExpression(textBox);
string passHex = (string)textBox.GetValue(PassColorProperty);
string failHex = (string)textBox.GetValue(FailColorProperty);
Color passColor = Extensions.ToColorFromHex(passHex);
Color failColor = Extensions.ToColorFromHex(failHex);
if (maskExpression == null)
{
return;
}
var proposedText = GetProposedText(textBox, e.Text);
if (!maskExpression.IsMatch(proposedText))
{
textBox.Background = new SolidColorBrush(failColor);
ValidationEventArgs args = new ValidationEventArgs();
args.sender = textBox;
args.valid = false;
OnValidation(args);
}
else
{
textBox.Background = new SolidColorBrush(passColor);
ValidationEventArgs args = new ValidationEventArgs();
args.sender = textBox;
args.valid = true;
OnValidation(args);
}
}
Event Called from the above code
public static event EventHandler<ValidationEventArgs> Validated;
static void OnValidation(ValidationEventArgs e)
{
EventHandler<ValidationEventArgs> handler = Validated;
if (handler != null)
{
handler(null, e);
}
}
public class ValidationEventArgs : EventArgs
{
public TextBox sender;
public bool valid;
}
Yes, I would argue that this violates MVVM. Your view model should have no knowledge of the views whatsoever. The question to always ask yourself is "can I run my application without creating any views?". In this case your view model is interacting directly with a list of TextBoxes, so the pattern is broken.
There are several ways of achieving your goal here, probably the most simple is to create a handler in your view model that gets called when your TextBox text changes:
public delegate void ValidationDelegate(bool isValid);
public class MyViewModel : ViewModelBase
{
public ValidationDelegate ValidationHandler { get { return (isValid) => OnValidate(isValid); } }
private void OnValidate(bool isValid)
{
// handle the validation event here
}
}
Now all you need is a behavior with an attached property that you can bind to this handler:
public class ValidateBehavior : Behavior<TextBox>
{
public ValidationDelegate Validated
{
get { return (ValidationDelegate)GetValue(ValidatedProperty); }
set { SetValue(ValidatedProperty, value); }
}
public static readonly DependencyProperty ValidatedProperty =
DependencyProperty.Register("Validated", typeof(ValidationDelegate), typeof(ValidateBehavior), new PropertyMetadata(null));
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.TextChanged += ValidateText;
}
protected override void OnDetaching()
{
base.OnDetaching();
this.AssociatedObject.TextChanged -= ValidateText;
}
private void ValidateText(object sender, TextChangedEventArgs e)
{
if (this.Validated != null)
{
bool isValid = true; // do text validation here
this.Validated(isValid);
}
}
}
And then finally add the behaviour to the TextBox in question and bind the handler:
<TextBox>
<i:Interaction.Behaviors>
<behaviors:ValidateBehavior Validated="{Binding ValidationHandler}"/>
</i:Interaction.Behaviors>
</TextBox>
EDIT: If you don't want to use a Blend behaviour then you can also do it with an attached behaviour:
public static class ValidateBehavior
{
public static ValidationDelegate GetValidate(TextBox textbox)
{
return (ValidationDelegate)textbox.GetValue(ValidateProperty);
}
public static void SetValidate(TextBox textbox, ValidationDelegate value)
{
textbox.SetValue(ValidateProperty, value);
}
public static readonly DependencyProperty ValidateProperty =
DependencyProperty.RegisterAttached(
"Validate",
typeof(ValidationDelegate),
typeof(ValidateBehavior),
new UIPropertyMetadata(null, OnValidateChanged));
static void OnValidateChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
var textbox = depObj as TextBox;
if (textbox == null)
return;
if (e.OldValue is ValidationDelegate)
textbox.TextChanged -= OnTextChanged;
if (e.NewValue is ValidationDelegate)
textbox.TextChanged += OnTextChanged;
}
static void OnTextChanged(object sender, RoutedEventArgs e)
{
if (!Object.ReferenceEquals(sender, e.OriginalSource))
return;
var textbox = e.OriginalSource as TextBox;
if (textbox != null)
{
var validate = GetValidate(textbox);
if (validate != null)
{
bool isValid = true; // do text validation here
validate(isValid);
}
}
}
}
And the corresponding XAML:
<TextBox behaviors:ValidateBehavior.Validate="{Binding ValidationHandler}" />

DependencyProperty Object not updating to UI

I am struggling with a dependencyproperty in a control. My dependencyproperty is an object which looks like this:
public class ChartGroupCollection : ObservableCollection<ChartGroup>, INotifyCollectionChanged
{
public void ClearDirty()
{
foreach (var grp in base.Items)
{
foreach(var run in grp.ChartRuns.Where(x=>x.IsDirty))
{
run.IsDirty = false;
}
grp.IsDirty = false;
}
}
[XmlIgnore]
public bool IsDirty //dirty flag for save prompt
{
get
{
....
}
}
}
[Serializable]
public class ChartGroup : INotifyPropertyChanged
{ ... //various properties }
The DependencyProperty is set up as here (named Tree, which is instance of a ChartGroupCollection):
public static readonly DependencyProperty TreeProperty = DependencyProperty.Register("Tree", typeof(ChartGroupCollection), typeof(ChartsControl), new PropertyMetadata(OnTreeChanged));
public ChartGroupCollection Tree
{
get { return (ChartGroupCollection)GetValue(TreeProperty); }
set { SetValue(TreeProperty, value); }
}
private static void OnTreeChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var Treee = sender as ChartsControl;
if (e.OldValue != null)
{
var coll = (INotifyCollectionChanged)e.OldValue;
coll.CollectionChanged -= Tree_CollectionChanged;
}
if (e.NewValue != null)
{
var coll = (ObservableCollection<ChartGroup>)e.NewValue;
coll.CollectionChanged += Tree_CollectionChanged;
}
}
private static void Tree_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
(sender as ChartsControl).OnTreeChanged();
}
void OnTreeChanged()
{
MessageBox.Show("Do something..."); //RefreshCharts();
}
I seem to be getting to the OnTreeChanged event only at creation of the object, but once i do other work (adding to lists inside ChartGroup or changing properties of ChartGroup objects or even deleting elements of the observablecollection it never seems to trigger a refresh event. I have tried several other methods of getting the dependencyproperty found online but no solution worked for me. I wonder if it is down to the intrinsic nature of my dependencyproperty object or an error from my side
The sender argument in your Tree_CollectionChanged handler is not the ChartsControl instance, but the collection that raised the CollectionChanged event.
The Tree_CollectionChanged method should not be static
private void Tree_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
OnTreeChanged();
}
and it should be attached and removed like this:
private static void OnTreeChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var control = sender as ChartsControl;
var oldCollection = e.OldValue as INotifyCollectionChanged;
var newCollection = e.NewValue as INotifyCollectionChanged;
if (oldCollection != null)
{
oldCollection.CollectionChanged -= control.Tree_CollectionChanged;
}
if (newCollection != null)
{
newCollection.CollectionChanged += control.Tree_CollectionChanged;
}
}
Note also that adding a CollectionChanged handler does not care for subscribing to PropertyChanged events of the collection elements. You would also have to attach or remove a PropertyChanged handler whenever an element is added to or removed from the collection. Take a look at the NotifyCollectionChangedEventArgs.Action property.

How to set the margin on a internal TextBoxView in wpf

I have a case where I want to minimize the horizontal padding of a textbox.
Using snoop I found that the textbox consists of a multiple sub-controls.
One of them is a TextBoxView with a margin of 2,0,2,0
The TextBoxView is an internal wpf component and has no public API.
How would you approach getting rid of the "internal padding"??
Set the outer margin to -2,0,-2,0 to compensate for the padding.
I created a custom control that removes that internal padding.
public class MyTextBox : TextBox
{
public MyTextBox()
{
Loaded += OnLoaded;
}
void OnLoaded(object sender, RoutedEventArgs e)
{
// the internal TextBoxView has a margin of 2,0,2,0 that needs to be removed
var contentHost = Template.FindName("PART_ContentHost", this) as ScrollViewer;
if (contentHost != null && contentHost.Content != null && contentHost.Content is FrameworkElement)
{
var textBoxView = contentHost.Content as FrameworkElement;
textBoxView.Margin = new Thickness(0,0,0,0);
}
}
}
Here is a dirty way of doing it:
public static class TextBoxView
{
public static readonly DependencyProperty MarginProperty = DependencyProperty.RegisterAttached(
"Margin",
typeof(Thickness?),
typeof(TextBoxView),
new PropertyMetadata(null, OnTextBoxViewMarginChanged));
public static void SetMargin(TextBox element, Thickness? value)
{
element.SetValue(MarginProperty, value);
}
[AttachedPropertyBrowsableForChildren(IncludeDescendants = false)]
[AttachedPropertyBrowsableForType(typeof(TextBox))]
public static Thickness? GetMargin(TextBox element)
{
return (Thickness?)element.GetValue(MarginProperty);
}
private static void OnTextBoxViewMarginChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var textBox = (TextBox)d;
OnTextBoxViewMarginChanged(textBox, (Thickness?)e.NewValue);
}
private static void OnTextBoxViewMarginChanged(TextBox textBox, Thickness? margin)
{
if (!textBox.IsLoaded)
{
textBox.Dispatcher.BeginInvoke(
DispatcherPriority.Loaded,
new Action(() => OnTextBoxViewMarginChanged(textBox, margin)));
return;
}
var textBoxView = textBox.NestedChildren()
.SingleOrDefault(x => x.GetType().Name == "TextBoxView");
if (margin == null)
{
textBoxView?.ClearValue(FrameworkElement.MarginProperty);
}
else
{
textBoxView?.SetValue(FrameworkElement.MarginProperty, margin);
}
}
private static IEnumerable<DependencyObject> NestedChildren(this DependencyObject parent)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
yield return child;
if (VisualTreeHelper.GetChildrenCount(child) == 0)
{
continue;
}
foreach (var nestedChild in NestedChildren(child))
{
yield return nestedChild;
}
}
}
}
It allows setting the margin on textboxes:
<Style TargetType="{x:Type TextBox}">
<Setter Property="demo:TextBoxView.Margin" Value="1,0" />
</Style>
Not optimized for performance at all.

WPF bing maps control polylines/polygons not draw on first add to collection

I'm working on this surface project where we have a bing maps control and where we would like to draw polylines on the map, by using databinding.
The strange behaviour that's occuring is that when I click the Add button, nothing happens on the map. If I move the map little bit, the polyline is drawn on the map. Another scenario that kind of works, is click the add button once, nothing happens, click it again both polylines are drawn. (In my manual collection I have 4 LocationCollections) so the same happens for the 3rd click and the fourth click where again both lines are drawn.
I have totally no idea where to look anymore to fix this. I have tried subscribing to the Layoutupdated events, which occur in both cases. Also added a collectionchanged event to the observablecollection to see if the add is triggered, and yes it is triggered. Another thing I tried is changing the polyline to pushpin and take the first location from the collection of locations in the pipelineviewmodel, than it's working a expected.
I have uploaded a sample project for if you want to see yourself what's happening.
Really hope that someone can point me in the right direction, because i don't have a clue anymore.
Below you find the code that i have written:
I have the following viewmodels:
MainViewModel
public class MainViewModel
{
private ObservableCollection<PipelineViewModel> _pipelines;
public ObservableCollection<PipelineViewModel> Pipes
{
get { return _pipelines; }
}
public MainViewModel()
{
_pipelines = new ObservableCollection<PipelineViewModel>();
}
}
And the PipelineViewModel which has the collection of Locations which implements INotifyPropertyChanged:
PipelineViewModel
public class PipelineViewModel : ViewModelBase
{
private LocationCollection _locations;
public string Geometry { get; set; }
public string Label { get; set; }
public LocationCollection Locations
{
get { return _locations; }
set
{
_locations = value;
RaisePropertyChanged("Locations");
}
}
}
My XAML looks like below:
<s:SurfaceWindow x:Class="SurfaceApplication3.SurfaceWindow1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="http://schemas.microsoft.com/surface/2008"
xmlns:m="clr-namespace:Microsoft.Maps.MapControl.WPF;assembly=Microsoft.Maps.MapControl.WPF"
Title="SurfaceApplication3">
<s:SurfaceWindow.Resources>
<DataTemplate x:Key="Poly">
<m:MapPolyline Locations="{Binding Locations}" Stroke="Black" StrokeThickness="5" />
</DataTemplate>
</s:SurfaceWindow.Resources>
<Grid>
<m:Map ZoomLevel="8" Center="52.332074,5.542302" Name="Map">
<m:MapItemsControl Name="x" ItemsSource="{Binding Pipes}" ItemTemplate="{StaticResource Poly}" />
</m:Map>
<Button Name="add" Width="100" Height="50" Content="Add" Click="add_Click"></Button>
</Grid>
</s:SurfaceWindow>
And in our codebehind we are setting up the binding and the click event like this:
private int _counter = 0;
private string[] geoLines;
private MainViewModel _mainViewModel = new MainViewModel();
/// <summary>
/// Default constructor.
/// </summary>
public SurfaceWindow1()
{
InitializeComponent();
// Add handlers for window availability events
AddWindowAvailabilityHandlers();
this.DataContext = _mainViewModel;
geoLines = new string[4]{ "52.588032,5.979309; 52.491143,6.020508; 52.397391,5.929871; 52.269838,5.957336; 52.224435,5.696411; 52.071065,5.740356",
"52.539614,4.902649; 52.429222,4.801025; 52.308479,4.86145; 52.246301,4.669189; 52.217704,4.836731; 52.313516,5.048218",
"51.840869,4.394531; 51.8731,4.866943; 51.99841,5.122375; 52.178985,5.438232; 51.8731,5.701904; 52.071065,6.421509",
"51.633362,4.111633; 51.923943,6.193542; 52.561325,5.28717; 52.561325,6.25946; 51.524125,5.427246; 51.937492,5.28717" };
}
private void add_Click(object sender, RoutedEventArgs e)
{
PipelineViewModel plv = new PipelineViewModel();
plv.Locations = AddLinestring(geoLines[_counter]);
plv.Geometry = geoLines[_counter];
_mainViewModel.Pipes.Add(plv);
_counter++;
}
private LocationCollection AddLinestring(string shapegeo)
{
LocationCollection shapeCollection = new LocationCollection();
string[] lines = Regex.Split(shapegeo, ";");
foreach (string line in lines)
{
string[] pts = Regex.Split(line, ",");
double lon = double.Parse(pts[1], new CultureInfo("en-GB"));
double lat = double.Parse(pts[0], new CultureInfo("en-GB"));
shapeCollection.Add(new Location(lat, lon));
}
return shapeCollection;
}
I did some digging on this problem and found that there is a bug in the Map implementation. I also made a workaround for it which can be used like this
<m:Map ...>
<m:MapItemsControl Name="x"
behaviors:MapFixBehavior.FixUpdate="True"/>
</m:Map>
I included this fix in your sample application and uploaded it here: SurfaceApplication3.zip
The visual tree for each ContentPresenter looks like this
When you add a new item to the collection the Polygon gets the wrong Points initially. Instead of values like 59, 29 it gets something like 0.0009, 0.00044.
The points are calculated in MeasureOverride in MapShapeBase and the part that does the calculation looks like this
MapMath.TryLocationToViewportPoint(ref this._NormalizedMercatorToViewport, location, out point2);
Initially, _NormalizedMercatorToViewport will have its default values (everything is set to 0) so the calculations goes all wrong. _NormalizedMercatorToViewport gets set in the method SetView which is called from MeasureOverride in MapLayer.
MeasureOverride in MapLayer has the following two if statements.
if ((element is ContentPresenter) && (VisualTreeHelper.GetChildrenCount(element) > 0))
{
child.SetView(...)
}
This comes out as false because the ContentPresenter hasn't got a visual child yet, it is still being generated. This is the problem.
The second one looks like this
IProjectable projectable2 = element as IProjectable;
if (projectable2 != null)
{
projectable2.SetView(...);
}
This comes out as false as well because the element, which is a ContentPresenter, doesn't implement IProjectable. This is implemented by the child MapShapeBase and once again, this child hasn't been generated yet.
So, SetView never gets called and _NormalizedMercatorToViewport in MapShapeBase will have its default values and the calculations goes wrong the first time when you add a new item.
Workaround
To workaround this problem we need to force a re-measure of the MapLayer. This has to be done when a new ContentPresenter is added to the MapItemsControl but after the ContentPresenter has a visual child.
One way to force an update is to create an attached property which has the metadata-flags AffectsRender, AffectsArrange and AffectsMeasure set to true. Then we just change the value of this property everytime we want to do the update.
Here is an attached behavior which does this. Use it like this
<m:Map ...>
<m:MapItemsControl Name="x"
behaviors:MapFixBehavior.FixUpdate="True"/>
</m:Map>
MapFixBehavior
public class MapFixBehavior
{
public static DependencyProperty FixUpdateProperty =
DependencyProperty.RegisterAttached("FixUpdate",
typeof(bool),
typeof(MapFixBehavior),
new FrameworkPropertyMetadata(false,
OnFixUpdateChanged));
public static bool GetFixUpdate(DependencyObject mapItemsControl)
{
return (bool)mapItemsControl.GetValue(FixUpdateProperty);
}
public static void SetFixUpdate(DependencyObject mapItemsControl, bool value)
{
mapItemsControl.SetValue(FixUpdateProperty, value);
}
private static void OnFixUpdateChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
MapItemsControl mapItemsControl = target as MapItemsControl;
ItemsChangedEventHandler itemsChangedEventHandler = null;
itemsChangedEventHandler = (object sender, ItemsChangedEventArgs ea) =>
{
if (ea.Action == NotifyCollectionChangedAction.Add)
{
EventHandler statusChanged = null;
statusChanged = new EventHandler(delegate
{
if (mapItemsControl.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
{
mapItemsControl.ItemContainerGenerator.StatusChanged -= statusChanged;
int index = ea.Position.Index + ea.Position.Offset;
ContentPresenter contentPresenter =
mapItemsControl.ItemContainerGenerator.ContainerFromIndex(index) as ContentPresenter;
if (VisualTreeHelper.GetChildrenCount(contentPresenter) == 1)
{
MapLayer mapLayer = GetVisualParent<MapLayer>(mapItemsControl);
mapLayer.ForceMeasure();
}
else
{
EventHandler layoutUpdated = null;
layoutUpdated = new EventHandler(delegate
{
if (VisualTreeHelper.GetChildrenCount(contentPresenter) == 1)
{
contentPresenter.LayoutUpdated -= layoutUpdated;
MapLayer mapLayer = GetVisualParent<MapLayer>(mapItemsControl);
mapLayer.ForceMeasure();
}
});
contentPresenter.LayoutUpdated += layoutUpdated;
}
}
});
mapItemsControl.ItemContainerGenerator.StatusChanged += statusChanged;
}
};
mapItemsControl.ItemContainerGenerator.ItemsChanged += itemsChangedEventHandler;
}
private static T GetVisualParent<T>(object childObject) where T : Visual
{
DependencyObject child = childObject as DependencyObject;
while ((child != null) && !(child is T))
{
child = VisualTreeHelper.GetParent(child);
}
return child as T;
}
}
MapLayerExtensions
public static class MapLayerExtensions
{
private static DependencyProperty ForceMeasureProperty =
DependencyProperty.RegisterAttached("ForceMeasure",
typeof(int),
typeof(MapLayerExtensions),
new FrameworkPropertyMetadata(0,
FrameworkPropertyMetadataOptions.AffectsRender |
FrameworkPropertyMetadataOptions.AffectsArrange |
FrameworkPropertyMetadataOptions.AffectsMeasure));
private static int GetForceMeasure(DependencyObject mapLayer)
{
return (int)mapLayer.GetValue(ForceMeasureProperty);
}
private static void SetForceMeasure(DependencyObject mapLayer, int value)
{
mapLayer.SetValue(ForceMeasureProperty, value);
}
public static void ForceMeasure(this MapLayer mapLayer)
{
SetForceMeasure(mapLayer, GetForceMeasure(mapLayer) + 1);
}
}

How to scroll to the bottom of a ScrollViewer automatically with Xaml and binding?

I've got a TextBlock whose content is data bound to a string property of the ViewModel. This TextBlock has a ScrollViewer wrapped around it.
What I want to do is every time the logs change, the ScrollViewer will scroll to the bottom. Ideally I want something like this:
<ScrollViewer ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollPosition="{Binding Path=ScrollPosition}">
<TextBlock Text="{Binding Path=Logs}"/>
</ScrollViewer>
I don't want to use Code Behind! The solution I'm looking for should be using only binding and/or Xaml.
You can either create an attached property or a behavior to achieve what you want without using code behind. Either way you will still need to write some code.
Here is an example of using attached property.
Attached Property
public static class Helper
{
public static bool GetAutoScroll(DependencyObject obj)
{
return (bool)obj.GetValue(AutoScrollProperty);
}
public static void SetAutoScroll(DependencyObject obj, bool value)
{
obj.SetValue(AutoScrollProperty, value);
}
public static readonly DependencyProperty AutoScrollProperty =
DependencyProperty.RegisterAttached("AutoScroll", typeof(bool), typeof(Helper), new PropertyMetadata(false, AutoScrollPropertyChanged));
private static void AutoScrollPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var scrollViewer = d as ScrollViewer;
if (scrollViewer != null && (bool)e.NewValue)
{
scrollViewer.ScrollToBottom();
}
}
}
Xaml Binding
<ScrollViewer local:Helper.AutoScroll="{Binding IsLogsChangedPropertyInViewModel}" .../>
You will need to create a boolean property IsLogsChangedPropertyInViewModel and set it to true when the string property is changed.
Hope this helps! :)
Answer updated 2017-12-13, now uses the ScrollChanged event and checks if the size of extent changes. More reliable and doesn't interfere with manual scrolling
I know this question is old, but I've got an improved implementation:
No external dependencies
You only need to set the property once
The code is heavily influenced by Both Justin XL's and Contango's solutions
public static class AutoScrollBehavior
{
public static readonly DependencyProperty AutoScrollProperty =
DependencyProperty.RegisterAttached("AutoScroll", typeof(bool), typeof(AutoScrollBehavior), new PropertyMetadata(false, AutoScrollPropertyChanged));
public static void AutoScrollPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
var scrollViewer = obj as ScrollViewer;
if(scrollViewer != null && (bool)args.NewValue)
{
scrollViewer.ScrollChanged += ScrollViewer_ScrollChanged;
scrollViewer.ScrollToEnd();
}
else
{
scrollViewer.ScrollChanged-= ScrollViewer_ScrollChanged;
}
}
private static void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
// Only scroll to bottom when the extent changed. Otherwise you can't scroll up
if (e.ExtentHeightChange != 0)
{
var scrollViewer = sender as ScrollViewer;
scrollViewer?.ScrollToBottom();
}
}
public static bool GetAutoScroll(DependencyObject obj)
{
return (bool)obj.GetValue(AutoScrollProperty);
}
public static void SetAutoScroll(DependencyObject obj, bool value)
{
obj.SetValue(AutoScrollProperty, value);
}
}
Usage:
<ScrollViewer n:AutoScrollBehavior.AutoScroll="True" > // Where n is the XML namespace
From Geoff's Blog on ScrollViewer AutoScroll Behavior.
Add this class:
namespace MyAttachedBehaviors
{
/// <summary>
/// Intent: Behavior which means a scrollviewer will always scroll down to the bottom.
/// </summary>
public class AutoScrollBehavior : Behavior<ScrollViewer>
{
private double _height = 0.0d;
private ScrollViewer _scrollViewer = null;
protected override void OnAttached()
{
base.OnAttached();
this._scrollViewer = base.AssociatedObject;
this._scrollViewer.LayoutUpdated += new EventHandler(_scrollViewer_LayoutUpdated);
}
private void _scrollViewer_LayoutUpdated(object sender, EventArgs e)
{
if (Math.Abs(this._scrollViewer.ExtentHeight - _height) > 1)
{
this._scrollViewer.ScrollToVerticalOffset(this._scrollViewer.ExtentHeight);
this._height = this._scrollViewer.ExtentHeight;
}
}
protected override void OnDetaching()
{
base.OnDetaching();
if (this._scrollViewer != null)
{
this._scrollViewer.LayoutUpdated -= new EventHandler(_scrollViewer_LayoutUpdated);
}
}
}
}
This code depends Blend Behaviors, which require a reference to System.Windows.Interactivity. See help on adding System.Windows.Interactivity.
If you install the MVVM Light NuGet package, you can add a reference here:
packages\MvvmLightLibs.4.2.30.0\lib\net45\System.Windows.Interactivity.dll
Ensure that you have this property in your header, which points to System.Windows.Interactivity.dll:
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
Add a Blend Behavior into the ScrollViewer:
<i:Interaction.Behaviors>
<implementation:AutoScrollBehavior />
</i:Interaction.Behaviors>
Example:
<GroupBox Grid.Row="2" Header ="Log">
<ScrollViewer>
<i:Interaction.Behaviors>
<implementation:AutoScrollBehavior />
</i:Interaction.Behaviors>
<TextBlock Margin="10" Text="{Binding Path=LogText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextWrapping="Wrap"/>
</ScrollViewer>
</GroupBox>
We have to add a definition for the namespace, or else it won't know where to find the C# class we have just added. Add this property into the <Window> tag. If you are using ReSharper, it will automatically suggest this for you.
xmlns:implementation="clr-namespace:MyAttachedBehaviors"
Now, if all goes well, the text in the box will always scroll down to the bottom.
The example XAML given will print the contents of the bound property LogText to the screen, which is perfect for logging.
It is easy, examples:
yourContronInside.ScrollOwner.ScrollToEnd ();
yourContronInside.ScrollOwner.ScrollToBottom ();
Here is a slight variation.
This will scroll to the bottom both when the scroll viewer height (viewport) and the height of it's scroll presenter's content (extent) change.
It's based on Roy T's answer but I wasn't able to comment so I have posted as an answer.
public static class AutoScrollHelper
{
public static readonly DependencyProperty AutoScrollProperty =
DependencyProperty.RegisterAttached("AutoScroll", typeof(bool), typeof(AutoScrollHelper), new PropertyMetadata(false, AutoScrollPropertyChanged));
public static void AutoScrollPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
var scrollViewer = obj as ScrollViewer;
if (scrollViewer == null) return;
if ((bool) args.NewValue)
{
scrollViewer.ScrollChanged += ScrollViewer_ScrollChanged;
scrollViewer.ScrollToEnd();
}
else
{
scrollViewer.ScrollChanged -= ScrollViewer_ScrollChanged;
}
}
static void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
// Remove "|| e.ViewportHeightChange < 0 || e.ExtentHeightChange < 0" if you want it to only scroll to the bottom when it increases in size
if (e.ViewportHeightChange > 0 || e.ExtentHeightChange > 0 || e.ViewportHeightChange < 0 || e.ExtentHeightChange < 0)
{
var scrollViewer = sender as ScrollViewer;
scrollViewer?.ScrollToEnd();
}
}
public static bool GetAutoScroll(DependencyObject obj)
{
return (bool) obj.GetValue(AutoScrollProperty);
}
public static void SetAutoScroll(DependencyObject obj, bool value)
{
obj.SetValue(AutoScrollProperty, value);
}
}
I was using #Roy T. 's answer, however I wanted the added stipulation that if you scrolled back in time, but then added text, the scroll view should auto scroll to bottom.
I used this:
private static void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
var scrollViewer = sender as ScrollViewer;
if (e.ExtentHeightChange > 0)
{
scrollViewer.ScrollToEnd();
}
}
in place of the SizeChanged event.

Categories

Resources