I'm trying to make some sort of wheel spinning. I have 5 customized text blocks, text file with the list of values (it may consist of 1-1000 items). After reading the file I have a 'List fileValues' with its values. I decided to create another 'List wheel' which will contain up to 5 elements at the time and is expected to be bind to text blocks.
When one presses a spin button, last element of 'wheel' is removed and new element from 'values' is added to the beginning of the 'wheel' list.
In order UI will be responsive to changes in the list, it is good to bind each element in the 'wheel' to corresponding text block on UI. But what I tried to do up to this moment didn't work.
Here is what I tried to do (the code is a little bit dirty, but I try to make it work firstly).
5 customized text blocks
<TextBlock Name="Value1" TextWrapping="WrapWithOverflow"/>
<TextBlock Name="Value2" TextWrapping="WrapWithOverflow"/>
<TextBlock Name="Value3" TextWrapping="WrapWithOverflow"/>
<TextBlock Name="Value4" TextWrapping="WrapWithOverflow"/>
<TextBlock Name="Value5" TextWrapping="WrapWithOverflow"/>
ObservableList which implements INotifyCollectionChanged interface
class ObservableList : INotifyCollectionChanged, IEnumerable
{
private readonly List<string> _valuesList;
public string First
{
get { return _valuesList.First(); }
}
public string Last
{
get { return _valuesList.Last(); }
}
public ObservableList()
{
this._valuesList = new List<string>();
}
public string this[Int32 index]
{
get
{
if (_valuesList.Count == 0 || index + 1 > _valuesList.Count)
{
return "------";
}
return _valuesList[index];
}
}
public void AddLast(string value)
{
_valuesList.Add(value);
OnNotifyCollectionChanged();
}
public void AddFirst(string value)
{
_valuesList.Insert(0, value);
OnNotifyCollectionChanged();
}
public void RemoveFirst()
{
_valuesList.RemoveAt(0);
OnNotifyCollectionChanged();
}
public void RemoveLast()
{
_valuesList.Remove(_valuesList.Last());
OnNotifyCollectionChanged();
}
public event NotifyCollectionChangedEventHandler CollectionChanged;
public void OnNotifyCollectionChanged()
{
if (CollectionChanged != null)
{
CollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
public IEnumerator GetEnumerator()
{
return (_valuesList as IEnumerable).GetEnumerator();
}
}
XAML Code-behind
public partial class MainWindow : Window
{
private List<string> _values = new List<string>();
private ObservableList _uiValues = new ObservableList();
public MainWindow()
{
InitializeComponent();
Value1.DataContext = _uiValues[0];
Value2.DataContext = _uiValues[1];
Value3.DataContext = _uiValues[2];
Value4.DataContext = _uiValues[3];
Value5.DataContext = _uiValues[4];
}
private void LoadFileBtn_Click(object sender, RoutedEventArgs e)
{
//Loads text file and fills _values
}
private void SpinBtn_Click(object sender, RoutedEventArgs e)
{
InitUiTextBlocks();
//Spin simulation
}
private void InitUiTextBlocks()
{
_uiValues.Clear();
for (int i = 0; i < 5; ++i)
{
//Nothing appears on UI and CollectionChanged event is null
_uiValues.AddLast(_values.First());
_values.RemoveAt(0);
}
}
}
I tried to use 'ObservableCollection', but the effect is the same. Nothing appears on UI. In fact I can't imagine how to bind each of List element to specific Label. Is it even possible to do such binding?
In the XAML do something like:
<Label Name="some_name" Content="{Binding SomeStingProperty}"/>
and in the code behind, have a
public string SomeStringProperty {get; set;}
you can bind to a collection as well, and if it's an ObservableCollection it will update on change.
search for basic XAML binding otherwise :)
(on a side note, it's cleaner i think it the XAML, i personally don't like to do it in the code behind ...)
As a side note, and totally self promoting, here are 2 articles that will probably help:
Understanding selected value
The big mvvm template.
The second might be a bit over your head if you're a beginner, but should be worth reading nevertheless.
Related
I have a WPF application where I communicate with a second application via TCP sockets. Each message sent and received has a given length and information, which is wrapped into an object.
After handling the message, this object is added to an ObservableList, which is (oneway) bound to a CollectionViewSource. This CollectionViewSource is the Itemsource of a Datagrid. Additionally, the CollectionViewSource has a filter which can be set with several toggleButtons. When the connection is closed, the list should be cleared for further use after reconnecting.
I tested this setup and especially the filtering up to a few thousand entries and it seems to work fine. Except if I clear the ObservableCollection, the whole interface becomes unresponsive, slow and setting the filter will spike the processing power used up to 60%, even for 10 objects in the list.
Clearing the ObservableCollection doesn't seem to update the Interface, only overwriting the Collection with a new object.
I also tried to change properties in XAML, like binding mode or read only, but this does change nothing.
<CollectionViewSource x:Key="telegramList"
Source="{Binding Path=LogManager.TList, mode=OneWay }"
Filter="telegramFilter">
</CollectionViewSource>
<DataGrid ItemsSource="{Binding Source={StaticResource telegramList}}" IsReadOnly="True" IsSynchronizedWithCurrentItem="False" x:Name="
Edit: Here is the LogManager, and the functions accessing the list. LogTelegram() can be called from either a Socket Callback, or directly from the main thread, before the message object is queued for sending.
public class LogManager : INotifyPropertyChanged
{
private ObservableCollection<ImmutableTelegram> tList;
public ObservableCollection<ImmutableTelegram> TList { get { return tList; } set { tList = value; NotifyPropertyChanged("TList"); } }
public void logTelegram(Telegram512 telegram)
{
if (!Model.Instance.gui.Dispatcher.CheckAccess())
{
Model.Instance.gui.Dispatcher.Invoke(delegate
{
logTelegramDelegate(telegram);
});
}
else
{
logTelegramDelegate(telegram);
}
}
private void logTelegramDelegate(Telegram512 telegram)
{
if (telegram.TELETYPE == Telegram512.Types.XX)
TList.Add(new ImmutableTelegram(telegram));
if(TList != null)
TList.Add(new ImmutableTelegram(telegram));
}
public void clearTelegramLog()
{
TList = null;
}
public void createTelegramLog()
{
TList = new ObservableCollection<ImmutableTelegram>();
}
}
Here is also the filter function and one state changed listener for a toggle button.
private void dataGridFilter(object sender, FilterEventArgs e)
{
var obj = e.Item as Telegram512;
if (obj != null)
{
e.Accepted = true;
if (obj.TELETYPE == Telegram512.Types.LF && this.lfToggle.IsChecked == false)
{
e.Accepted = false;
return;
}
}
}
private void lfToggle_Click(object sender, RoutedEventArgs e)
{
if(telegramDataGrid.ItemsSource != null)
CollectionViewSource.GetDefaultView(telegramDataGrid.ItemsSource).Refresh();
}
I'm trying to build an application that the user points to a folder of PDF files.
of Invoices, the program then parses the PDF files to find out wich ones contains an email address and wich ones don't. and this Is Where I'm stuck:
I then want to add the file names to either the Listbox for print or the Listbox for email.
I got all the other bits working, choosing the folder and parsing the PDF and adding the folder path to a textbox object.
I then run a function:
private void listFiles(string selectedPath)
{
string[] fileEntries = Directory.GetFiles(selectedPath);
foreach (string files in fileEntries)
{
try
{
ITextExtractionStrategy its = new iTextSharp.text.pdf.parser.LocationTextExtractionStrategy();
using (PdfReader reader = new PdfReader(files))
{
string thePage = PdfTextExtractor.GetTextFromPage(reader, 1, its);
string[] theLines = thePage.Split('\n');
if (theLines[1].Contains("#"))
{
// System.Windows.MessageBox.Show("denne fil kan sendes som email til " + theLines[1], "Email!");
}
else
{
System.Windows.MessageBox.Show("denne fil skal Printes da " + theLines[1] + " ikke er en email", "PRINT!");
}
}
}
catch (Exception exc)
{
System.Windows.MessageBox.Show("FEJL!", exc.Message);
}
}
}
And it is in this function I want to be able to add the files to either Listbox.
My XAML looks like this:
<Grid.Resources>
<local:ListofPrint x:Key="listofprint"/>
</Grid.Resources>
<ListBox x:Name="lbxPrint" ItemsSource="{StaticResource listofprint}" HorizontalAlignment="Left" Height="140" Margin="24.231,111.757,0,0" VerticalAlignment="Top" Width="230"/>
But I get the error: The name "ListofPrint" does not exist in the namespace "clr-namespace:test_app".
the ListofPrint is here:
public class ListofPrint : ObservableCollection<PDFtoPrint>
{
public ListofPrint(string xfile)
{
Add(new PDFtoPrint(xfile));
}
}
I've been trying to get the hang of the documentation on MSDN and have read 10 different similar Questions on this site, but I guess my problem is that I don't know exactly what my problem is. first of it's a data binding problem but I basically copied the sample from the documentation to play with but that is what is giving me the trouble.
Hopefully, someone here can explain to me the basics of data binding and how it corresponds to my ObservableCollection.
You need to create an instance of your collection class and bind the ListBox to it.
The most simple thing is setting its DataContext to this. I wrote an example:
Window:
public class MyWindow : Window
{
// must be a property! This is your instance...
public YourCollection MyObjects {get; } = new YourCollection();
public MyWindow()
{
// set datacontext to the window's instance.
this.DataContext = this;
InitializeComponent();
}
public void Button_Click(object sender, EventArgs e)
{
// add an object to your collection (instead of directly to the listbox)
MyObjects.AddTitle("Hi There");
}
}
Your notifyObject collection:
public class YourCollection : ObservableCollection<MyObject>
{
// some wrapper functions for example:
public void Add(string title)
{
this.Add(new MyObject { Title = title });
}
}
Item class:
// by implementing the INotifyPropertyChanged, changes to properties
// will update the listbox on-the-fly
public class MyObject : INotifyPropertyChanged
{
private string _title;
// a property.
public string Title
{
get { return _title;}
set
{
if(_title!= value)
{
_title = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs( nameof(Title)));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
Xaml:
<ListBox ItemsSource="{Binding MyObjects}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Title}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I have observable collection called (Users) in view model that binded with ListViewControl (lstUsers) in view and what I need is to scroll to current logged in user in List View .
I see in most of examples that used scroll from code behind as following e.g. :
lstUsers.ScrollIntoView(lstUsers[5]);
but what I need is to handle it from view model .
Please advice !
One way of doing this would be to use something like an ICollectionView which has a current item. You can then set IsSynchronizedWithCurrentItem to true to link the current item in the view model to the selected item in the ListView.
Finally handle the event SelectionChanged in the code behind the view to change the scroll position so that it always displays the selected item.
For me the benefit of this method is that the viewmodel is kept unaware of anything about the view which is one of the aims of MVVM. The code behind the view is the perfect place for any code concerning the view only.
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ListView x:Name="View"
SelectionChanged="Selector_OnSelectionChanged" IsSynchronizedWithCurrentItem="True" ItemsSource="{Binding Items}"/>
<Button Grid.Row="1" Command="{Binding ChangeSelectionCommand}">Set</Button>
</Grid>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel();
}
private void Selector_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
View.ScrollIntoView(View.SelectedItem);
}
}
public class ViewModel
{
private readonly CollectionViewSource _source = new CollectionViewSource();
public ICollectionView Items
{
get { return _source.View; }
}
public ICommand ChangeSelectionCommand { get; set; }
public ViewModel()
{
SetUp();
ChangeSelectionCommand = new Command(ChangeSelection);
}
private void SetUp()
{
var list = new List<string>();
for (int i = 0; i < 100; i++)
{
list.Add(i.ToString(CultureInfo.InvariantCulture));
}
_source.Source = list;
}
private void ChangeSelection()
{
var random = new Random(DateTime.Now.Millisecond);
var n = random.Next(100);
Items.MoveCurrentToPosition(n);
}
}
public class Command : ICommand
{
private readonly Action _action;
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
_action();
}
public event EventHandler CanExecuteChanged;
public Command(Action action)
{
_action = action;
}
}
let me share my solution with you
Create your own ListView descendant with dependency property TargetListItem
public class ScrollableListView : ListView
{
/// <summary>
/// Set this property to make ListView scroll to it
/// </summary>
public object TargetListItem
{
get { return (object)GetValue(TargetListItemProperty); }
set { SetValue(TargetListItemProperty, value); }
}
public static readonly DependencyProperty TargetListItemProperty = DependencyProperty.Register(
nameof(TargetListItem), typeof(object), typeof(ScrollableListView), new PropertyMetadata(null, TargetListItemPropertyChangedCallback));
static void TargetListItemPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var owner = (ScrollableListView)d;
owner.ScrollToItem(e.NewValue);
}
public void ScrollToItem(object value)
{
if (value != null && Items != null && Items.Contains(value))
{
ScrollIntoView(value);
}
}
}
create property in ViewModel
object currentListItem;
public object СurrentListItem
{
get => сurrentListItem;
set
{
if (сurrentListItem != value)
{
сurrentListItem = value;
OnPropertyChanged(nameof(СurrentListItem));
}
}
}
bind it
<controls:ScrollableListView ... TargetListItem="{Binding CurrentListItem}"/>
Now you can set CurrentListItem in ViewModel when needed. And the corresponding visual element will become visible in the ListView immediately.
Also maybe you just can use attached property on ListView instead of creating ScrollableListView. But i'm not sure.
Yep, there's always times in MVVM when you need to get at the control. There's various ways of doing this, but here's an easy-ish way of doing it without deriving from the control or messing with routed commands or other such toys what you have in WPF.
In summary:
Create an attached property on your view model.
Set the attached property in XAML to pass the list box back to the view model.
Call .ScrollIntoView on demand.
Note, this is a rough and ready example, make sure your DataContext is set before showing the window.
Code/View Model:
public class ViewModel
{
private ListBox _listBox;
private void ReceiveListBox(ListBox listBox)
{
_listBox = listBox;
}
public static readonly DependencyProperty ListBoxHookProperty = DependencyProperty.RegisterAttached(
"ListBoxHook", typeof (ListBox), typeof (ViewModel), new PropertyMetadata(default(ListBox), ListBoxHookPropertyChangedCallback));
private static void ListBoxHookPropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
var listBox = (ListBox) dependencyObject;
var viewModel = (ViewModel) listBox.DataContext;
viewModel.ReceiveListBox(listBox);
}
public static void SetListBoxHook(DependencyObject element, ListBox value)
{
element.SetValue(ListBoxHookProperty, value);
}
public static ListBox GetListBoxHook(DependencyObject element)
{
return (ListBox) element.GetValue(ListBoxHookProperty);
}
}
OK, so that will let us get the ListBox passed back to the view; you can do with it as you wish.
Now, just set the property in XAML:
<ListBox wpfApplication1:ViewModel.ListBoxHook="{Binding RelativeSource={RelativeSource Self}}" />
Good to go!
I am trying to set up a multi-language application, so when the user changes the display language all the texts in all the open windows change automatically.
I am having issues through with binding combo-box control. The binding needs to be done in code-behind as I have dynamic content coming from a database, and sometimes I even have to create additional combo-boxes at runtime.
Also I do not want to keep the translations in the database because I do not want to query the database every time a user is changing the display language.
What I did until now:
in xaml:
<ComboBox x:Name="cmb"/>
and in C#:
public class MyCmbItem
{
public int Index { get; set; }
public string Text { get; set; }
}
private ObservableCollection<MyCmbItem> LoadText()
{
ObservableCollection<MyCmbItem> _result = new ObservableCollection<MyCmbItem>();
foreach (var _item in _list)
{
//the list is coming from a database read
_result.Add(new MyCmbItem { Index = _item.Value, Text = _res_man_global.GetString(_item.KeyText, _culture) });
}
return _result;
}
public ObservableCollection<MyCmbItem> MyTexts
{
get { return LoadText(); }
set {} //I do not have to add/remove items at runtime so for now I leave this empty
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
...
LoadList(); //this adds values in _list
cmb.ItemsSource = MyTexts; //this populates the combo-box
Here I got stuck and I do not know how to determine the combo-box to refresh the displayed texts. The method must achieve that if I have several windows opened each containing a random number of combo-boxes, when I change the current language all the combo-boxes in all the windows will refresh the displayed list, without affecting other values inside (like the selected item). Does anybody know how this can be done?
Many thanks.
For your xaml UI, the INotifyPropertyChanged interface indicates updates of the viewmodel. You can extend your class like this:
public class MyCmbItem : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string APropertyName)
{
var property_changed = PropertyChanged;
if (property_changed != null)
{
property_changed(this, new PropertyChangedEventArgs(APropertyName));
}
}
private string _Text;
private string _KeyText;
public int Index { get; set; }
public string Text
{
get { return _Text;}
set {
if (_Text != value)
{
_Text = value;
NotifyPropertyChanged("Text");
}
}
}
public MyCmbItem(string key_text, int index)
{
Index = index;
_KeyText = key_text;
RefreshText();
_res_man_global.LanguageChanged += () => RefreshText();
}
public void RefreshText()
{
Text = _res_man_global.GetString(_KeyText, _culture);
}
}
Your view can simply bind to the Text-property as following:
<DataTemplate DataType="{x:Type local:MyCmbItem}">
<TextBlock Text="{Binding Path=Text}"/>
</DataTemplate>
Note: I assumed that your language class is global and has some kind of language-changed notification event.
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);
}
}