Windows Phone 8.1 WinRT memory leak with ObservableCollection - c#

I'm working with large amount of objects (POI's) that are getting displayed on a MapControl. I'm helping myself with a MVVM Light to obey the rules of the MVVM approach.
Since I'm obligated to display every object on the map, I have to use MapItemsControl collection, and not the MapElements one.
This collection binds to the the ObservableCollection<PushpinViewModel> object (Pushpins) in corresponding ViewModel. Everything works as expected, up to the point, when I want to refresh Pushpins. The problem is memory leak. But first, some code to visualize the problem:
XAML:
<maps:MapControl x:Name="Map"
x:Uid="MapControl">
<maps:MapItemsControl ItemsSource="{Binding Pushpins}">
<maps:MapItemsControl.ItemTemplate>
<DataTemplate>
<Image Source="{Binding Image}"/>
</DataTemplate>
</maps:MapItemsControl.ItemTemplate>
</maps:MapItemsControl>
MainViewModel:
public class MainViewModel : ViewModelBase
{
public RelayCommand AddCommand { get; set; }
public RelayCommand ClearCommand { get; set; }
public RelayCommand CollectCommand { get; set; }
public ObservableCollection<PushpinViewModel> Pushpins { get; set; }
/* Ctor, initialization of Pushpins and stuff like that */
private void Collect()
{
GC.Collect(2);
GC.WaitForPendingFinalizers();
GC.Collect(2);
PrintCurrentMemory();
}
private void Clear()
{
Pushpins.Clear();
PrintCurrentMemory();
}
private void Add()
{
for (int i = 0; i < 1000; i++)
{
Pushpins.Add(new PushpinViewModel());
}
PrintCurrentMemory();
}
private void PrintCurrentMemory()
{
Logger.Log(String.Format("Total Memory: {0}", GC.GetTotalMemory(true) / 1024.0));
}
}
PushpinViewModel:
public class PushpinViewModel: ViewModelBase
{
public string Image { get { return "/Assets/SomeImage.png"; } }
~PushpinViewModel()
{
Logger.Log("This finalizer never gets called!");
}
}
Now, consider the following scenario. I add to the Pushpins collection 1000 PushpinViewModel elements. They are rendered, memory is allocated, everything's fine. Now I want to clear the collection, and add another (different in real scenario) 1000 elements. So, I call Clear() method. But.. nothing happens! Pushpins gets cleared, but PushpinViewModel's finalizers are not called! Then I add 1000 elements again, and my memory usage doubles.
You can guess what happens next. When I repeat this Clear() - Add() procedure 3-5 times my app crashes.
So, what is the problem? Clearly ObservableCollection is holding references to the PushpinViewModel objects after Clear() has been performed on it, so they cannot be garbage collected. Of course forcing GC to perform garbage collection does not help (it sometimes even makes the situation worse).
It's bothering me for 2 days now, I have tried many different scenarios to try and overcome this problem, but to be honest, nothing helped.
There was only one thing worth nothing - I don't remember the exact scenario, but when I had assigned Pushpins = null, and then did something more, VehiceViewModel's were destroyed. But that does not work for me, because I also remember that I had problem with visualizing these pins on the map after the Clear().
Do you have any ideas what can cause this memory leak? How can I force OC's members to destroy? Maybe there is some kind of alternative for OC?
Thanks in advance for any help!
EDIT:
I did some tests with XAML Map Control - https://xamlmapcontrol.codeplex.com/, and results are surprising. Overall map performance with >1000 elements added to it, is poorer than a native MapControl, BUT, if I call Add() x1000, then Clear(), then Add() x1000, the PushpinViewModel's finalizers are geting called! Memory gets freed, and app does not crash. So there is definitely something wrong with Microsoft's MapControl...

OK, here's the behavior I made that emulates what MapItemsControl does. Note that this is pretty untested -- it works in my app, but has really not been tried anywhere else. And I have never tested the RemoveItems function because my app just adds items to an ObservableCollection and clears them; it never removes items incrementally.
Also note that it tags the XAML pushpins with the hash code of the item it is bound to; this is how it identifies which pushpins to remove from the map if the collection changes. This may not work for your circumstances, but it seems to be effective.
Usage:
Note: NumberedCircle is a user control that is simply a red circle that displays a number inside of it; replace with whatever XAML control you want to use as a pushpin. Destinations is my ObservableCollection of objects that have a Number property (to display inside the pushpin) and a Point property (the pushpin location).
<map:MapControl>
<i:Interaction.Behaviors>
<behaviors:PushpinCollectionBehavior ItemsSource="{Binding Path=Destinations}">
<behaviors:PushpinCollectionBehavior.ItemTemplate>
<DataTemplate>
<controls:NumberedCircle Number="{Binding Path=Number}" map:MapControl.Location="{Binding Path=Point}" />
</DataTemplate>
</behaviors:PushpinCollectionBehavior.ItemTemplate>
</behaviors:PushpinCollectionBehavior>
</i:Interaction.Behaviors>
</map:MapControl>
Code:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Xaml.Interactivity;
using Windows.Devices.Geolocation;
using Windows.Foundation;
using Windows.Storage.Streams;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls.Maps;
namespace Foo.Behaviors
{
/// <summary>
/// Behavior to draw pushpins on a map. This effectively replaces MapItemsControl, which is flaky as hell.
/// </summary>
public class PushpinCollectionBehavior : DependencyObject, IBehavior
{
#region IBehavior
public DependencyObject AssociatedObject { get; private set; }
public void Attach(Windows.UI.Xaml.DependencyObject associatedObject)
{
var mapControl = associatedObject as MapControl;
if (mapControl == null)
throw new ArgumentException("PushpinCollectionBehavior can be attached only to MapControl");
AssociatedObject = associatedObject;
mapControl.Unloaded += MapControlUnloaded;
}
public void Detach()
{
var mapControl = AssociatedObject as MapControl;
if (mapControl != null)
mapControl.Unloaded -= MapControlUnloaded;
}
#endregion
#region Dependency Properties
/// <summary>
/// The dependency property of the item that contains the pushpin locations.
/// </summary>
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(object), typeof(PushpinCollectionBehavior), new PropertyMetadata(null, OnItemsSourcePropertyChanged));
/// <summary>
/// The item that contains the pushpin locations.
/// </summary>
public object ItemsSource
{
get { return GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
/// <summary>
/// Adds, moves, or removes the pushpin when the item source changes.
/// </summary>
private static void OnItemsSourcePropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
var behavior = dependencyObject as PushpinCollectionBehavior;
var mapControl = behavior.AssociatedObject as MapControl;
// add the items
if (behavior.ItemsSource is IList)
behavior.AddItems(behavior.ItemsSource as IList);
else
throw new Exception("PushpinCollectionBehavior needs an IList as the items source.");
// subscribe to changes in the collection
if (behavior.ItemsSource is INotifyCollectionChanged)
{
var items = behavior.ItemsSource as INotifyCollectionChanged;
items.CollectionChanged += behavior.CollectionChanged;
}
}
// <summary>
/// The dependency property of the pushpin template.
/// </summary>
public static readonly DependencyProperty ItemTemplateProperty =
DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(PushpinCollectionBehavior), new PropertyMetadata(null));
/// <summary>
/// The pushpin template.
/// </summary>
public DataTemplate ItemTemplate
{
get { return (DataTemplate)GetValue(ItemTemplateProperty); }
set { SetValue(ItemTemplateProperty, value); }
}
#endregion
#region Events
/// <summary>
/// Adds or removes the items on the map.
/// </summary>
private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
AddItems(e.NewItems);
break;
case NotifyCollectionChangedAction.Remove:
RemoveItems(e.OldItems);
break;
case NotifyCollectionChangedAction.Reset:
ClearItems();
break;
}
}
/// <summary>
/// Removes the CollectionChanged event handler from the ItemsSource when the map is unloaded.
/// </summary>
void MapControlUnloaded(object sender, RoutedEventArgs e)
{
var items = ItemsSource as INotifyCollectionChanged;
if (items != null)
items.CollectionChanged -= CollectionChanged;
}
#endregion
#region Private Functions
/// <summary>
/// Adds items to the map.
/// </summary>
private void AddItems(IList items)
{
var mapControl = AssociatedObject as MapControl;
foreach (var item in items)
{
var templateInstance = ItemTemplate.LoadContent() as FrameworkElement;
var hashCode = item.GetHashCode();
templateInstance.Tag = hashCode;
templateInstance.DataContext = item;
mapControl.Children.Add(templateInstance);
Tags.Add(hashCode);
}
}
/// <summary>
/// Removes items from the map.
/// </summary>
private void RemoveItems(IList items)
{
var mapControl = AssociatedObject as MapControl;
foreach (var item in items)
{
var hashCode = item.GetHashCode();
foreach (var child in mapControl.Children.Where(c => c is FrameworkElement))
{
var frameworkElement = child as FrameworkElement;
if (hashCode.Equals(frameworkElement.Tag))
{
mapControl.Children.Remove(frameworkElement);
continue;
}
}
Tags.Remove(hashCode);
}
}
/// <summary>
/// Clears items from the map.
/// </summary>
private void ClearItems()
{
var mapControl = AssociatedObject as MapControl;
foreach (var tag in Tags)
{
foreach (var child in mapControl.Children.Where(c => c is FrameworkElement))
{
var frameworkElement = child as FrameworkElement;
if (tag.Equals(frameworkElement.Tag))
{
mapControl.Children.Remove(frameworkElement);
continue;
}
}
}
Tags.Clear();
}
#endregion
#region Private Properties
/// <summary>
/// The object tags of the items this behavior has placed on the map.
/// </summary>
private List<int> Tags
{
get
{
if (_tags == null)
_tags = new List<int>();
return _tags;
}
}
private List<int> _tags;
#endregion
}
}

Related

PRISM 5 MEF AvalonDock 2.0 DataAdapter Registered Views and Parent IsSelected

I am attempting to build an MVVM Windows Application Using PRISM 5 and I have wrapped my main content window with an AvalonDock (Wrapping Code Below).
using Microsoft.Practices.Prism.Regions;
using Xceed.Wpf.AvalonDock;
using System.Collections.Specialized;
using System.Windows;
using Xceed.Wpf.AvalonDock.Layout;
namespace Central.Adapters
{
using System.Linq;
public class AvalonDockRegionAdapter : RegionAdapterBase<DockingManager>
{
/// <summary>
/// This ties the adapter into the base region factory.
/// </summary>
/// <param name="factory">The factory that determines where the modules will go.</param>
public AvalonDockRegionAdapter(IRegionBehaviorFactory factory)
: base(factory)
{
}
/// <summary>
/// Since PRISM does not support the Avalon DockingManager natively this adapter provides the needed support.
/// </summary>
/// <param name="region">This is the region that resides in the DockingManager.</param>
/// <param name="regionTarget">The DockingManager that needs the window added.</param>
protected override void Adapt(IRegion region, DockingManager regionTarget)
{
region.Views.CollectionChanged += (sender, e) =>
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
AddAnchorableDocument(regionTarget, e);
break;
case NotifyCollectionChangedAction.Remove:
break;
}
};
}
/// <summary>
/// This adds the window as an anchorable document to the Avalon DockingManager.
/// </summary>
/// <param name="regionTarget">The DockingManager instance.</param>
/// <param name="e">The new window to be added.</param>
private static void AddAnchorableDocument(DockingManager regionTarget, NotifyCollectionChangedEventArgs e)
{
foreach (FrameworkElement element in e.NewItems)
{
var view = element as UIElement;
var documentPane = regionTarget.Layout.Descendents().OfType<LayoutDocumentPane>().FirstOrDefault();
if ((view == null) || (documentPane == null))
{
continue;
}
var newContentPane = new LayoutAnchorable
{
Content = view,
Title = element.ToolTip.ToString(),
CanHide = true,
CanClose = false
};
documentPane.Children.Add(newContentPane);
}
}
/// <summary>
/// This returns the region instance populated with all of its contents.
/// </summary>
/// <returns>DockingManager formatted region.</returns>
protected override IRegion CreateRegion()
{
return new AllActiveRegion();
}
}
}
I then register this adapter in the bootstrapper this way:
protected override RegionAdapterMappings ConfigureRegionAdapterMappings()
{
var mappings = base.ConfigureRegionAdapterMappings();
if (mappings == null)
{
return null;
}
mappings.RegisterMapping(typeof(DockingManager), new AvalonDockRegionAdapter(ConfigureDefaultRegionBehaviors()));
return mappings;
}
The problem that I am facing is that other region UI elements will require certain LayoutAnchorable windows to become the active and selected window. The content I am feeding into the LayoutAnchorable object is a ContentControl.
In my View's ViewModel I have a property that I am successfully setting using another UI element's interaction. However I am unable to make the connection from ViewModel(Property) -> ContentContro(View) -> LayoutAnchorable(View's Parent).IsSelected or ,IsActive.
I know how to bind to a parent object but that eats up the property and does not allow me to bind it to the ViewModel property as well. I also have no problem binding to a ViewModel property, but that is useless unless I can get it to set the parent property. I have also attempted View based events. Problem with this is that once the view loads it doe not like calling its own events anymore unless it is caused by user interaction directly with that view.
In short I just want to display the appropriate window when needed based on an interaction in another part of my program. Maybe I am making this more complicated than it needs to be. Any assistance on this would be greatly appreciated.
Thanks
James
As I took a break from the problem at had I looked at it from another perspective. To solve the issue I decided to store the instance of the content panes containing the views into a singleton dictionary class:
using System;
using System.Collections.Generic;
using Xceed.Wpf.AvalonDock.Layout;
namespace Central.Services
{
public class DockinWindowChildObjectDictionary
{
private static Dictionary<string, LayoutAnchorable> _contentPane = new Dictionary<string, LayoutAnchorable>();
private static readonly Lazy<DockinWindowChildObjectDictionary> _instance =
new Lazy<DockinWindowChildObjectDictionary>(()=> new DockinWindowChildObjectDictionary(), true);
public static DockinWindowChildObjectDictionary Instance
{
get
{
return _instance.Value;
}
}
/// <summary>
/// Causes the constructor to be private allowing for proper use of the Singleton pattern.
/// </summary>
private DockinWindowChildObjectDictionary()
{
}
/// <summary>
/// Adds a Content Pane instance to the dictionary.
/// </summary>
/// <param name="title">The title given to the Pane during instantiation.</param>
/// <param name="contentPane">The object instance.</param>
public static void Add(string title, LayoutAnchorable contentPane)
{
_contentPane.Add(title, contentPane);
}
/// <summary>
/// If a window needs to be removed from the dock this should be used
/// to also remove it from the dictionary.
/// </summary>
/// <param name="title">The title given to the Pane during instantiation.</param>
public static void Remove(string title)
{
_contentPane.Remove(title);
}
/// <summary>
/// This will return the instance of the content pane that holds the view.
/// </summary>
/// <param name="title">The title given to the Pane during instantiation.</param>
/// <returns>The views Parent Instance.</returns>
public static LayoutAnchorable GetInstance(string title)
{
return _contentPane[title];
}
}
}
In the adapter I modified this code as follows:
private static void AddAnchorableDocument(DockingManager regionTarget, NotifyCollectionChangedEventArgs e)
{
foreach (FrameworkElement element in e.NewItems)
{
var view = element as UIElement;
var documentPane = regionTarget.Layout.Descendents().OfType<LayoutDocumentPane>().FirstOrDefault();
if ((view == null) || (documentPane == null))
{
continue;
}
var newContentPane = new LayoutAnchorable
{
Content = view,
Title = element.ToolTip.ToString(),
CanHide = true,
CanClose = false
};
DockinWindowChildObjectDictionary.Add(element.ToolTip.ToString(),** newContentPane);
documentPane.Children.Add(newContentPane);
}
}
Then I added the following to the ViewModel to gain the effect I was going after:
public void OnNavigatedTo(NavigationContext navigationContext)
{
var viewParentInstance = DockinWindowChildObjectDictionary.GetInstance("Belt Plan");
viewParentInstance.IsSelected = true;
}
One hurdle done and on to the next. For a base to all the information in this post the ViewSwitchingNavigation.sln included with the PRISM 5.0 download will get you started. If you are wondering about the ConfigureDefaultRegionBehaviors() referenced in the adapter registration I got that from the StockTraderRI_Desktop.sln in the sample downloads.
I hope this post helps someone else that finds themselves in the same pickle this technology sandwich provides.
Sincerely
James

Bind value from one class to another value in another class

I have the following classes gist with the classes.
I want to bind Item.Visible to Items.ItemsVisible - is it possible?, if so - how?
Item.cs:
using System;
using System.ComponentModel;
namespace WpfApplication85
{
/// <summary>
/// Item Object.
/// </summary>
public class Item : INotifyPropertyChanged
{
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged; //Event to notify when Property changed.
/// <summary>
/// Notify that Property has Changed.
/// </summary>
/// <param name="propertyName">The name of the Property</param>
protected void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
#region Private Variables
private bool _Visible; //Bool to determine if the Item is visible or not
#endregion
#region Public Properties
//Return the value of Visible / Set the value of Visible and Notify.
public bool Visible
{
get { return _Visible; }
set
{
_Visible = value;
NotifyPropertyChanged("Visible");
}
}
#endregion
#region Constructor
/// <summary>
/// Item Constructor
/// </summary>
public Item()
{
_Visible = true;
}
#endregion
}
}
Items.cs:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace WpfApplication85
{
/// <summary>
/// Items Object.
/// </summary>
public class Items : INotifyPropertyChanged
{
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged; //Event to notify when Property changed.
/// <summary>
/// Notify that Property has Changed.
/// </summary>
/// <param name="propertyName">The name of the Property</param>
protected void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
#region Private Variables
private bool _itemsVisible; //Bool to determine if the Items are visible or not
private ObservableCollection<Item> _itemsCollection; //Collection of Items.
#endregion
#region Public Properties
//Return the value of ItemsVisible / Set the value of ItemsVisible and Notify.
public bool ItemsVisible
{
get { return _itemsVisible; }
set
{
_itemsVisible = value;
NotifyPropertyChanged("ItemsVisible");
}
}
//Return the Items Collection / Set the Items Collection and Notify.
public ObservableCollection<Item> ItemsCollection
{
get
{
return _itemsCollection;
}
set
{
_itemsCollection = value;
NotifyPropertyChanged("ItemsCollection");
}
}
#endregion
#region Constructor
/// <summary>
/// Items Constructor
/// </summary>
public Items()
{
_itemsVisible = true;
_itemsCollection = new ObservableCollection<Item>();
}
#endregion
#region Methods
/// <summary>
/// Add Item to the ItemsCollection.
/// </summary>
/// <param name="item">Item Object</param>
public void AddItem(Item item)
{
//Bind item.Visible to this.ItemsVisible
_itemsCollection.Add(item);
}
#endregion
}
}
Setting data binding within Items and Item properties is nothing but listening PropertyChanged or CollectionChanged event from proper interfaces.
You can use either += clause for the subscription, or WeakEventListener pattern, using the PropertyChangedEventManager and CollectionChangedEventManager
I prefer the last one, because:
Listening for events can lead to memory leaks.
So, your Items class should implement IWeakEventListener interface:
public class Items : INotifyPropertyChanged, IWeakEventListener
{
#region IWeakEventListener
public bool ReceiveWeakEvent(Type managerType, Object sender, EventArgs e)
{
if (sender == this._itemsCollection && managerType == typeof(CollectionChangedEventManager))
{
// Your collection has changed, you should add/remove
// subscription for PropertyChanged event
UpdateSubscriptions((NotifyCollectionChangedEventArgs)e);
return true;
}
if (sender is Item && managerType == typeof(PropertyChangedEventManager))
{
// The Visible property of an Item object has changed
// You should handle it properly here, for example, like this:
this.ItemsVisible = this._itemsCollection.All(i => i.Visible);
return true;
}
return false;
}
private void UpdateSubscriptions(NotifyCollectionChangedEventArgs e)
{
switch(e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (Item item in e.NewItems)
{
PropertyChangedEventManager.AddListener(item, this, "Visible");
}
break;
case NotifyCollectionChangedAction.Remove:
foreach (Item item in e.OldItems)
{
PropertyChangedEventManager.RemoveListener(item, this, "Visible");
}
break;
case NotifyCollectionChangedAction.Reset:
foreach (Item item in this._itemsCollection)
{
PropertyChangedEventManager.RemoveListener(item, this, "Visible");
PropertyChangedEventManager.AddListener(item, this, "Visible");
}
break;
default:
break;
}
}
...
public Items()
{
_itemsVisible = true;
_itemsCollection = new ObservableCollection<Item>();
CollectionChangedEventManager.AddListener(_itemsCollection, this);
}
}
Binding in the WPF sense only works on DependencyProperties, and does not apply between two standard properties (even when using INotifyPropertyChanged).
That said, if you are using these classes as View Models and are binding them to controls you could use a MultiConverter to set the visibility of the Control to collapsed when both the ItemsVisible and Visible property are true (for example).
Alternatively, you could add a Parent property to the Item class and set it to the parent Items class which would allow you to have the Item.Visible property return the parent's ItemsVisible property (or again whatever logic makes sense in your application).

Two Way Binding to AvalonEdit Document Text using MVVM

I want to include an AvalonEdit TextEditor control into my MVVM application. The first thing I require is to be able to bind to the TextEditor.Text property so that I can display text. To do this I have followed and example that was given in Making AvalonEdit MVVM compatible. Now, I have implemented the following class using the accepted answer as a template
public sealed class MvvmTextEditor : TextEditor, INotifyPropertyChanged
{
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string), typeof(MvvmTextEditor),
new PropertyMetadata((obj, args) =>
{
MvvmTextEditor target = (MvvmTextEditor)obj;
target.Text = (string)args.NewValue;
})
);
public new string Text
{
get { return base.Text; }
set { base.Text = value; }
}
protected override void OnTextChanged(EventArgs e)
{
RaisePropertyChanged("Text");
base.OnTextChanged(e);
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string info)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
Where the XAML is
<Controls:MvvmTextEditor HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
FontFamily="Consolas"
FontSize="9pt"
Margin="2,2"
Text="{Binding Text, NotifyOnSourceUpdated=True, Mode=TwoWay}"/>
Firstly, this does not work. The Binding is not shown in Snoop at all (not red, not anything, in fact I cannot even see the Text dependency property).
I have seen this question which is exactly the same as mine Two-way binding in AvalonEdit doesn't work but the accepted answer does not work (at least for me). So my question is:
How can I perform two way binding using the above method and what is the correct implementation of my MvvmTextEditor class?
Thanks for your time.
Note: I have my Text property in my ViewModel and it implements the required INotifyPropertyChanged interface.
Create a Behavior class that will attach the TextChanged event and will hook up the dependency property that is bound to the ViewModel.
AvalonTextBehavior.cs
public sealed class AvalonEditBehaviour : Behavior<TextEditor>
{
public static readonly DependencyProperty GiveMeTheTextProperty =
DependencyProperty.Register("GiveMeTheText", typeof(string), typeof(AvalonEditBehaviour),
new FrameworkPropertyMetadata(default(string), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, PropertyChangedCallback));
public string GiveMeTheText
{
get { return (string)GetValue(GiveMeTheTextProperty); }
set { SetValue(GiveMeTheTextProperty, value); }
}
protected override void OnAttached()
{
base.OnAttached();
if (AssociatedObject != null)
AssociatedObject.TextChanged += AssociatedObjectOnTextChanged;
}
protected override void OnDetaching()
{
base.OnDetaching();
if (AssociatedObject != null)
AssociatedObject.TextChanged -= AssociatedObjectOnTextChanged;
}
private void AssociatedObjectOnTextChanged(object sender, EventArgs eventArgs)
{
var textEditor = sender as TextEditor;
if (textEditor != null)
{
if (textEditor.Document != null)
GiveMeTheText = textEditor.Document.Text;
}
}
private static void PropertyChangedCallback(
DependencyObject dependencyObject,
DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
var behavior = dependencyObject as AvalonEditBehaviour;
if (behavior.AssociatedObject!= null)
{
var editor = behavior.AssociatedObject as TextEditor;
if (editor.Document != null)
{
var caretOffset = editor.CaretOffset;
editor.Document.Text = dependencyPropertyChangedEventArgs.NewValue.ToString();
editor.CaretOffset = caretOffset;
}
}
}
}
View.xaml
<avalonedit:TextEditor
WordWrap="True"
ShowLineNumbers="True"
LineNumbersForeground="Magenta"
x:Name="textEditor"
FontFamily="Consolas"
SyntaxHighlighting="XML"
FontSize="10pt">
<i:Interaction.Behaviors>
<controls:AvalonEditBehaviour GiveMeTheText="{Binding Test, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</i:Interaction.Behaviors>
</avalonedit:TextEditor>
i must be defined as
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
ViewModel.cs
private string _test;
public string Test
{
get { return _test; }
set { _test = value; }
}
That should give you the Text and push it back to the ViewModel.
Create a BindableAvalonEditor class with a two-way binding on the Text property.
I was able to establish a two-way binding with the latest version of AvalonEdit by combining Jonathan Perry's answer and 123 456 789 0's answer. This allows a direct two-way binding without the need for behaviors.
Here is the source code...
public class BindableAvalonEditor : ICSharpCode.AvalonEdit.TextEditor, INotifyPropertyChanged
{
/// <summary>
/// A bindable Text property
/// </summary>
public new string Text
{
get
{
return (string)GetValue(TextProperty);
}
set
{
SetValue(TextProperty, value);
RaisePropertyChanged("Text");
}
}
/// <summary>
/// The bindable text property dependency property
/// </summary>
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register(
"Text",
typeof(string),
typeof(BindableAvalonEditor),
new FrameworkPropertyMetadata
{
DefaultValue = default(string),
BindsTwoWayByDefault = true,
PropertyChangedCallback = OnDependencyPropertyChanged
}
);
protected static void OnDependencyPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
var target = (BindableAvalonEditor)obj;
if (target.Document != null)
{
var caretOffset = target.CaretOffset;
var newValue = args.NewValue;
if (newValue == null)
{
newValue = "";
}
target.Document.Text = (string)newValue;
target.CaretOffset = Math.Min(caretOffset, newValue.ToString().Length);
}
}
protected override void OnTextChanged(EventArgs e)
{
if (this.Document != null)
{
Text = this.Document.Text;
}
base.OnTextChanged(e);
}
/// <summary>
/// Raises a property changed event
/// </summary>
/// <param name="property">The name of the property that updates</param>
public void RaisePropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
I like none of these solutions. The reason the author didn't create a dependency property on Text is for performance reason. Working around it by creating an attached property means the text string must be recreated on every key stroke. On a 100mb file, this can be a serious performance issue. Internally, it only uses a document buffer and will never create the full string unless requested.
It exposes another property, Document, which is a dependency property, and it exposes the Text property to construct the string only when needed. Although you can bind to it, it would mean designing your ViewModel around a UI element which defeats the purpose of having a ViewModel UI-agnostic. I don't like that option either.
Honestly, the cleanest(ish) solution is to create 2 events in your ViewModel, one to display the text and one to update the text. Then you write a one-line event handler in your code-behind, which is fine since it's purely UI-related. That way, you construct and assign the full document string only when it's truly needed. Additionally, you don't even need to store (nor update) the text in the ViewModel. Just raise DisplayScript and UpdateScript when it is needed.
It's not an ideal solution, but there are less drawbacks than any other method I've seen.
TextBox also faces a similar issue, and it solves it by internally using a DeferredReference object that constructs the string only when it is really needed. That class is internal and not available to the public, and the Binding code is hard-coded to handle DeferredReference in a special way. Unfortunately there doesn't seen to be any way of solving the problem in the same way as TextBox -- perhaps unless TextEditor would inherit from TextBox.
Another nice OOP approach is to download the source code of AvalonEdit (it's open sourced), and creating a new class that inherits from TextEditor class (the main editor of AvalonEdit).
What you want to do is basically override the Text property and implement an INotifyPropertyChanged version of it, using dependency property for the Text property and raising the OnPropertyChanged event when text is changed (this can be done by overriding the OnTextChanged() method.
Here's a quick code (fully working) example that works for me:
public class BindableTextEditor : TextEditor, INotifyPropertyChanged
{
/// <summary>
/// A bindable Text property
/// </summary>
public new string Text
{
get { return base.Text; }
set { base.Text = value; }
}
/// <summary>
/// The bindable text property dependency property
/// </summary>
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string), typeof(BindableTextEditor), new PropertyMetadata((obj, args) =>
{
var target = (BindableTextEditor)obj;
target.Text = (string)args.NewValue;
}));
protected override void OnTextChanged(EventArgs e)
{
RaisePropertyChanged("Text");
base.OnTextChanged(e);
}
/// <summary>
/// Raises a property changed event
/// </summary>
/// <param name="property">The name of the property that updates</param>
public void RaisePropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
For those wondering about an MVVM implementation using AvalonEdit, here is one of the ways it can be done, first we have the class
/// <summary>
/// Class that inherits from the AvalonEdit TextEditor control to
/// enable MVVM interaction.
/// </summary>
public class CodeEditor : TextEditor, INotifyPropertyChanged
{
// Vars.
private static bool canScroll = true;
/// <summary>
/// Default constructor to set up event handlers.
/// </summary>
public CodeEditor()
{
// Default options.
FontSize = 12;
FontFamily = new FontFamily("Consolas");
Options = new TextEditorOptions
{
IndentationSize = 3,
ConvertTabsToSpaces = true
};
}
#region Text.
/// <summary>
/// Dependancy property for the editor text property binding.
/// </summary>
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string), typeof(CodeEditor),
new PropertyMetadata((obj, args) =>
{
CodeEditor target = (CodeEditor)obj;
target.Text = (string)args.NewValue;
}));
/// <summary>
/// Provide access to the Text.
/// </summary>
public new string Text
{
get { return base.Text; }
set { base.Text = value; }
}
/// <summary>
/// Return the current text length.
/// </summary>
public int Length
{
get { return base.Text.Length; }
}
/// <summary>
/// Override of OnTextChanged event.
/// </summary>
protected override void OnTextChanged(EventArgs e)
{
RaisePropertyChanged("Length");
base.OnTextChanged(e);
}
/// <summary>
/// Event handler to update properties based upon the selection changed event.
/// </summary>
void TextArea_SelectionChanged(object sender, EventArgs e)
{
this.SelectionStart = SelectionStart;
this.SelectionLength = SelectionLength;
}
/// <summary>
/// Event that handles when the caret changes.
/// </summary>
void TextArea_CaretPositionChanged(object sender, EventArgs e)
{
try
{
canScroll = false;
this.TextLocation = TextLocation;
}
finally
{
canScroll = true;
}
}
#endregion // Text.
#region Caret Offset.
/// <summary>
/// DependencyProperty for the TextEditorCaretOffset binding.
/// </summary>
public static DependencyProperty CaretOffsetProperty =
DependencyProperty.Register("CaretOffset", typeof(int), typeof(CodeEditor),
new PropertyMetadata((obj, args) =>
{
CodeEditor target = (CodeEditor)obj;
if (target.CaretOffset != (int)args.NewValue)
target.CaretOffset = (int)args.NewValue;
}));
/// <summary>
/// Access to the SelectionStart property.
/// </summary>
public new int CaretOffset
{
get { return base.CaretOffset; }
set { SetValue(CaretOffsetProperty, value); }
}
#endregion // Caret Offset.
#region Selection.
/// <summary>
/// DependencyProperty for the TextLocation. Setting this value
/// will scroll the TextEditor to the desired TextLocation.
/// </summary>
public static readonly DependencyProperty TextLocationProperty =
DependencyProperty.Register("TextLocation", typeof(TextLocation), typeof(CodeEditor),
new PropertyMetadata((obj, args) =>
{
CodeEditor target = (CodeEditor)obj;
TextLocation loc = (TextLocation)args.NewValue;
if (canScroll)
target.ScrollTo(loc.Line, loc.Column);
}));
/// <summary>
/// Get or set the TextLocation. Setting will scroll to that location.
/// </summary>
public TextLocation TextLocation
{
get { return base.Document.GetLocation(SelectionStart); }
set { SetValue(TextLocationProperty, value); }
}
/// <summary>
/// DependencyProperty for the TextEditor SelectionLength property.
/// </summary>
public static readonly DependencyProperty SelectionLengthProperty =
DependencyProperty.Register("SelectionLength", typeof(int), typeof(CodeEditor),
new PropertyMetadata((obj, args) =>
{
CodeEditor target = (CodeEditor)obj;
if (target.SelectionLength != (int)args.NewValue)
{
target.SelectionLength = (int)args.NewValue;
target.Select(target.SelectionStart, (int)args.NewValue);
}
}));
/// <summary>
/// Access to the SelectionLength property.
/// </summary>
public new int SelectionLength
{
get { return base.SelectionLength; }
set { SetValue(SelectionLengthProperty, value); }
}
/// <summary>
/// DependencyProperty for the TextEditor SelectionStart property.
/// </summary>
public static readonly DependencyProperty SelectionStartProperty =
DependencyProperty.Register("SelectionStart", typeof(int), typeof(CodeEditor),
new PropertyMetadata((obj, args) =>
{
CodeEditor target = (CodeEditor)obj;
if (target.SelectionStart != (int)args.NewValue)
{
target.SelectionStart = (int)args.NewValue;
target.Select((int)args.NewValue, target.SelectionLength);
}
}));
/// <summary>
/// Access to the SelectionStart property.
/// </summary>
public new int SelectionStart
{
get { return base.SelectionStart; }
set { SetValue(SelectionStartProperty, value); }
}
#endregion // Selection.
#region Properties.
/// <summary>
/// The currently loaded file name. This is bound to the ViewModel
/// consuming the editor control.
/// </summary>
public string FilePath
{
get { return (string)GetValue(FilePathProperty); }
set { SetValue(FilePathProperty, value); }
}
// Using a DependencyProperty as the backing store for FilePath.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty FilePathProperty =
DependencyProperty.Register("FilePath", typeof(string), typeof(CodeEditor),
new PropertyMetadata(String.Empty, OnFilePathChanged));
#endregion // Properties.
#region Raise Property Changed.
/// <summary>
/// Implement the INotifyPropertyChanged event handler.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged([CallerMemberName] string caller = null)
{
var handler = PropertyChanged;
if (handler != null)
PropertyChanged(this, new PropertyChangedEventArgs(caller));
}
#endregion // Raise Property Changed.
}
Then in your view where you want to have AvalonEdit, you can do
...
<Grid>
<Local:CodeEditor
x:Name="CodeEditor"
FilePath="{Binding FilePath,
Mode=TwoWay,
NotifyOnSourceUpdated=True,
NotifyOnTargetUpdated=True}"
WordWrap="{Binding WordWrap,
Mode=TwoWay,
NotifyOnSourceUpdated=True,
NotifyOnTargetUpdated=True}"
ShowLineNumbers="{Binding ShowLineNumbers,
Mode=TwoWay,
NotifyOnSourceUpdated=True,
NotifyOnTargetUpdated=True}"
SelectionLength="{Binding SelectionLength,
Mode=TwoWay,
NotifyOnSourceUpdated=True,
NotifyOnTargetUpdated=True}"
SelectionStart="{Binding SelectionStart,
Mode=TwoWay,
NotifyOnSourceUpdated=True,
NotifyOnTargetUpdated=True}"
TextLocation="{Binding TextLocation,
Mode=TwoWay,
NotifyOnSourceUpdated=True,
NotifyOnTargetUpdated=True}"/>
</Grid>
Where this can be placed in a UserControl or Window or what ever, then in the ViewModel for this view we have (where I am using Caliburn Micro for the MVVM framework stuff)
public string FilePath
{
get { return filePath; }
set
{
if (filePath == value)
return;
filePath = value;
NotifyOfPropertyChange(() => FilePath);
}
}
/// <summary>
/// Should wrap?
/// </summary>
public bool WordWrap
{
get { return wordWrap; }
set
{
if (wordWrap == value)
return;
wordWrap = value;
NotifyOfPropertyChange(() => WordWrap);
}
}
/// <summary>
/// Display line numbers?
/// </summary>
public bool ShowLineNumbers
{
get { return showLineNumbers; }
set
{
if (showLineNumbers == value)
return;
showLineNumbers = value;
NotifyOfPropertyChange(() => ShowLineNumbers);
}
}
/// <summary>
/// Hold the start of the currently selected text.
/// </summary>
private int selectionStart = 0;
public int SelectionStart
{
get { return selectionStart; }
set
{
selectionStart = value;
NotifyOfPropertyChange(() => SelectionStart);
}
}
/// <summary>
/// Hold the selection length of the currently selected text.
/// </summary>
private int selectionLength = 0;
public int SelectionLength
{
get { return selectionLength; }
set
{
selectionLength = value;
UpdateStatusBar();
NotifyOfPropertyChange(() => SelectionLength);
}
}
/// <summary>
/// Gets or sets the TextLocation of the current editor control. If the
/// user is setting this value it will scroll the TextLocation into view.
/// </summary>
private TextLocation textLocation = new TextLocation(0, 0);
public TextLocation TextLocation
{
get { return textLocation; }
set
{
textLocation = value;
UpdateStatusBar();
NotifyOfPropertyChange(() => TextLocation);
}
}
And that's it! Done.
I hope this helps.
Edit. for all those looking for an example of working with AvalonEdit using MVVM, you can download a very basic editor application from http://1drv.ms/1E5nhCJ.
Notes. This application actually creates a MVVM friendly editor control by inheriting from the AvalonEdit standard control and adds additional Dependency Properties to it as appropriate - *this is different to what I have shown in the answer given above*. However, in the solution I have also shown how this can be done (as I describe in the answer above) using Attached Properties and there is code in the solution under the Behaviors namespace. What is actually implemented however, is the first of the above approaches.
Please also be aware that there is some code in the solution that is unused. This *sample* was a stripped back version of a larger application and I have left some code in as it could be useful to the user who downloads this example editor. In addition to the above, in the example code I access the Text by binding to document, there are some purest that may argue that this is not pure-MVVM, and I say "okay, but it works". Some times fighting this pattern is not the way to go.
I hope this of use to some of you.

WPF Silverlight Datagrid in real time, refresh? Timer?

I have certain datagrid which I need to "refresh" every... lets say 1 min.
Is a timer the best option?
public PageMain()
{
InitializeComponent();
DataGridFill();
InitTimer();
}
private void InitTimer()
{
disTimer = new Timer(new TimeSpan(0, 1, 0).TotalMilliseconds);
disTimer.Elapsed += disTimer_Elapsed;
disTimer.Start();
}
void disTimer_Elapsed(object sender, ElapsedEventArgs e)
{
DataGridFill();
}
private void DataGridFill()
{
var items = GetItems(1);
ICollectionView itemsView =
CollectionViewSource.GetDefaultView(items);
itemsView.GroupDescriptions.Add(new PropertyGroupDescription("MyCustomGroup"));
// Set the view as the DataContext for the DataGrid
AmbientesDataGrid.DataContext = itemsView;
}
Is there a less "dirty" solution?
The best way to "Refresh" a DataGrid is to bind it to a collection of items, and update the source collection of items every X minutes.
<DataGrid ItemsSource="{Binding MyCollection}" ... />
This way you never have to reference the DataGrid itself, so your UI logic and application logic stay separated, and if your refresh takes a while you can run it on a background thread without locking up your UI.
Because WPF can't update objects created on one thread from another thread, you may want to get your data and store in a temporary collection on a background thread, then update your bound collection on the main UI thread.
For the timing bit, use a Timer or possibly a DispatcherTimer if needed.
var timer = new System.Windows.Threading.DispatcherTimer();
timer.Tick += Timer_Tick;
timer.Interval = new TimeSpan(0,1,0);
timer.Start();
private void Timer_Tick(object sender, EventArgs e)
{
MyCollection = GetUpdatedCollectionData();
}
My prefered approach:
public sealed class ViewModel
{
/// <summary>
/// As this is readonly, the list property cannot change, just it's content so
/// I don't need to send notify messages.
/// </summary>
private readonly ObservableCollection<T> _list = new ObservableCollection<T>();
/// <summary>
/// Bind to me.
/// I publish as IEnumerable<T>, no need to show your inner workings.
/// </summary>
public IEnumerable<T> List { get { return _list; } }
/// <summary>
/// Add items. Call from a despatch timer if you wish.
/// </summary>
/// <param name="newItems"></param>
public void AddItems(IEnumerable<T> newItems)
{
foreach(var item in newItems)
{
_list.Add(item);
}
}
/// <summary>
/// Sets the list of items. Call from a despatch timer if you wish.
/// </summary>
/// <param name="newItems"></param>
public void SetItems(IEnumerable<T> newItems)
{
_list.Clear();
AddItems(newItems);
}
}
Don't like lack of decent AddRange/ReplaceRange in ObservableCollection<T>? Me neither, but here is an descendant to ObservableCollection<T> to add a message efficient AddRange, plus unit tests:
ObservableCollection Doesn't support AddRange method, so I get notified for each item added, besides what about INotifyCollectionChanging?

ListView scroll to last item WPF C#

how to make scroll rule always at end of the ListView automatic without focus in WPF?
The answers from #pduncan and #MattBurland both have some problems, primarily that they fail to unregister the behaviour properly.
This implementation stores the handler in another attached property so that you can toggle the behaviour on and off, perhaps through a binding.
Note that this applies to ListView. If you are using ListBox, change occurrences of ListView to ListBox.
public static class ListViewExtensions
{
public static readonly DependencyProperty AutoScrollToEndProperty = DependencyProperty.RegisterAttached("AutoScrollToEnd", typeof(bool), typeof(ListViewExtensions), new UIPropertyMetadata(OnAutoScrollToEndChanged));
private static readonly DependencyProperty AutoScrollToEndHandlerProperty = DependencyProperty.RegisterAttached("AutoScrollToEndHandler", typeof(NotifyCollectionChangedEventHandler), typeof(ListViewExtensions));
public static bool GetAutoScrollToEnd(DependencyObject obj) => (bool)obj.GetValue(AutoScrollToEndProperty);
public static void SetAutoScrollToEnd(DependencyObject obj, bool value) => obj.SetValue(AutoScrollToEndProperty, value);
private static void OnAutoScrollToEndChanged(DependencyObject s, DependencyPropertyChangedEventArgs e)
{
var listView = s as ListView;
if (listView == null)
return;
var source = (INotifyCollectionChanged)listView.Items.SourceCollection;
if ((bool)e.NewValue)
{
NotifyCollectionChangedEventHandler scrollToEndHandler = delegate
{
if (listView.Items.Count <= 0)
return;
listView.Items.MoveCurrentToLast();
listView.ScrollIntoView(listView.Items.CurrentItem);
};
source.CollectionChanged += scrollToEndHandler;
listView.SetValue(AutoScrollToEndHandlerProperty, scrollToEndHandler);
}
else
{
var handler = (NotifyCollectionChangedEventHandler)listView.GetValue(AutoScrollToEndHandlerProperty);
source.CollectionChanged -= handler;
}
}
}
Use it like this:
<ListView local:ListViewExtensions.AutoScrollToEnd="{Binding Path=AutoScroll}">
I also made it a a static class, as you don't need to instantiate it (nor does it need to derive from DependencyObject). The cast to INotifyCollectionChanged was changed so that errors surface as cast exceptions, not NREs.
you can use this , its working for me :
this.myListView.ScrollIntoView(myListView.Items[myListView.Items.Count-1]);
if you need more advanced detail you can refere to this post
Since #pduncan's answer doesn't include the actual code, here is the code from the link they posted with only a minor modification from me:
public class ListBoxExtenders : DependencyObject
{
public static readonly DependencyProperty AutoScrollToEndProperty = DependencyProperty.RegisterAttached("AutoScrollToEnd", typeof(bool), typeof(ListBoxExtenders), new UIPropertyMetadata(default(bool), OnAutoScrollToEndChanged));
/// <summary>
/// Returns the value of the AutoScrollToEndProperty
/// </summary>
/// <param name="obj">The dependency-object whichs value should be returned</param>
/// <returns>The value of the given property</returns>
public static bool GetAutoScrollToEnd(DependencyObject obj)
{
return (bool)obj.GetValue(AutoScrollToEndProperty);
}
/// <summary>
/// Sets the value of the AutoScrollToEndProperty
/// </summary>
/// <param name="obj">The dependency-object whichs value should be set</param>
/// <param name="value">The value which should be assigned to the AutoScrollToEndProperty</param>
public static void SetAutoScrollToEnd(DependencyObject obj, bool value)
{
obj.SetValue(AutoScrollToEndProperty, value);
}
/// <summary>
/// This method will be called when the AutoScrollToEnd
/// property was changed
/// </summary>
/// <param name="s">The sender (the ListBox)</param>
/// <param name="e">Some additional information</param>
public static void OnAutoScrollToEndChanged(DependencyObject s, DependencyPropertyChangedEventArgs e)
{
var listBox = s as ListBox;
if (listBox != null)
{
var listBoxItems = listBox.Items;
var data = listBoxItems.SourceCollection as INotifyCollectionChanged;
var scrollToEndHandler = new System.Collections.Specialized.NotifyCollectionChangedEventHandler(
(s1, e1) =>
{
if (listBox.Items.Count > 0)
{
listBoxItems.MoveCurrentToLast();
listBox.ScrollIntoView(listBoxItems.CurrentItem);
}
});
if ((bool)e.NewValue)
data.CollectionChanged += scrollToEndHandler;
else
data.CollectionChanged -= scrollToEndHandler;
}
}
}
I modified it to use MoveCurrentToLast instead of getting the last item with listBox.Items[listBox.Items.Count - 1];. The reason for this (aside from being a little cleaner anyway) is that there is an edge case where if you have duplicate items in a list, then getting listBox.Items.Count - 1 and then scrolling to that item might not scroll you to the end of the list (it could even scroll you in the opposite direction!). It would instead scroll you to the first instance of that item.
Use an Attached Property. This article will show you how:
http://michlg.wordpress.com/2010/01/17/listbox-automatically-scroll-to-bottom/
For me, these work if each record in the list box is different. If they are the same it just scrolls to the first one that matches.

Categories