I am using the DateTimePicker WPF control from the Extended WPF Toolkit Community Edition library version 2.5.
My problem is that when I pick a date, the OnValueChanged event is raised twice instead of just once.
Here is the code I am using:
XAML:
<StackPanel>
<xctk:DateTimePicker AutoCloseCalendar="True" Name="picker" Width="400" Height="40" ValueChanged="UpDownBase_OnValueChanged"/>
<ListBox Height="300" Name="listbox"></ListBox>
</StackPanel>
C# code behind:
private void UpDownBase_OnValueChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
var value = picker.Value;
if (value == null)
listbox.Items.Add("[NULL]");
else
listbox.Items.Add(value.Value.ToString(CultureInfo.InvariantCulture));
}
Now, whenever I pick a new date, the list box will be populated with two new items. I also debugged the program and confirmed that the event handler is actually invoked twice.
How can I solve this issue?
UPDATE:
I tried version 2.4 and it seems that the issue is gone. It seems to me now that this might be a possible bug in version 2.5.
This would appear to be because the event in 2.5 is being fired from:
at Xceed.Wpf.Toolkit.DateTimePicker.OnValueChanged(Nullable`1 oldValue, Nullable`1 newValue) in C:\Users\Mark Vinten\Downloads\wpftoolkit-114314\Main\Source\ExtendedWPFToolkitSolution\Src\Xceed.Wpf.Toolkit\DateTimePicker\Implementation\DateTimePicker.cs:line 264
And then subsequently from the base class:
at Xceed.Wpf.Toolkit.TimePicker.OnValueChanged(Nullable`1 oldValue, Nullable`1 newValue) in C:\Users\Mark Vinten\Downloads\wpftoolkit-114314\Main\Source\ExtendedWPFToolkitSolution\Src\Xceed.Wpf.Toolkit\TimePicker\Implementation\TimePicker.cs:line 264
Now the base class, also seems to go through the CLR binding process suggesting that this is the bound value. I'm still looking into why that would be, but a workaround is to use Binding as such:
MainWindow.cs
public DateTime? DateTimeValue
{
get { return (DateTime?)GetValue(DateTimeValueProperty); }
set { SetValue(DateTimeValueProperty, value); }
}
// Using a DependencyProperty as the backing store for DateTimeValue. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DateTimeValueProperty =
DependencyProperty.Register("DateTimeValue", typeof(DateTime?), typeof(MainWindow), new PropertyMetadata(null, new PropertyChangedCallback(DateTimeValueProperty_Changed)));
private static void DateTimeValueProperty_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MainWindow mw = d as MainWindow;
System.Diagnostics.Debug.WriteLine("d is " + d == null ? "null" : d.GetType().FullName);
if (mw != null && e.Property == DateTimeValueProperty)
{
var value = e.NewValue as DateTime?;
var listbox = FindChild<ListBox>(mw, "listbox");
if (value == null)
listbox.Items.Add("[NULL]");
else
listbox.Items.Add(value.Value.ToString(System.Globalization.CultureInfo.InvariantCulture));
}
}
/// <summary>
/// Finds a Child of a given item in the visual tree.
/// </summary>
/// <param name="parent">A direct parent of the queried item.</param>
/// <typeparam name="T">The type of the queried item.</typeparam>
/// <param name="childName">x:Name or Name of child. </param>
/// <returns>The first parent item that matches the submitted type parameter.
/// If not matching item can be found,
/// a null parent is being returned.</returns>
public static T FindChild<T>(DependencyObject parent, string childName)
where T : DependencyObject
{
// Confirm parent and childName are valid.
if (parent == null) return null;
T foundChild = null;
int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < childrenCount; i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
// If the child is not of the request child type child
T childType = child as T;
if (childType == null)
{
// recursively drill down the tree
foundChild = FindChild<T>(child, childName);
// If the child is found, break so we do not overwrite the found child.
if (foundChild != null) break;
}
else if (!string.IsNullOrEmpty(childName))
{
var frameworkElement = child as FrameworkElement;
// If the child's name is set for search
if (frameworkElement != null && frameworkElement.Name == childName)
{
// if the child's name is of the request name
foundChild = (T)child;
break;
}
}
else
{
// child element found.
foundChild = (T)child;
break;
}
}
return foundChild;
}
MainWindow.xaml
<StackPanel DataContext="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:MainWindow}}}">
<xctk:DateTimePicker AutoCloseCalendar="True" Name="picker" Width="400" Height="40" Value="{Binding DateTimeValue}" />
<ListBox Height="300" Name="listbox"></ListBox>
</StackPanel>
This uses the binding system which automatically checks whether the value has changed and only raises events if it has.
Note: FindChild<> was a function that I found on this How can I find WPF controls by name or type? post
Update with final summary
The reason for this appears to be is because there is a TimePicker embedded within the DateTimePicker to provide the functionality. Unfortunately, both DateTimePicker and TimePicker derive from the same base and thus raise the same routed event within UpDownBase where T is DateTime?.
if you check on the event arguments, e.RoutedEVent is always UpDownBase.OnValueChanged since this is the class raising the event. e.Source or e.OriginalSource is always the DateTimePicker itself meaning you have no useful way to filter out one or the other event.
There is code within DateTimeUpDown.RaiseValueChangedEvent() to check if the TemplatedParent is a TimePicker to prevent re-raising but whether the event is raised from the DateTimePicker or the TimePicker the TemplatedParent always seems to be the DateTimePicker so that fails thus you get the event twice.
I have raised a bug with the findings on the WPFToolkit project site:
https://wpftoolkit.codeplex.com/workitem/22014
I solved this problem/bug by checking the originalsource of the event.
if(e.OriginalSource is Xceed.Wpf.Toolkit.DateTimePicker)
{
if(((Xceed.Wpf.Toolkit.DateTimePicker)e.OriginalSource).IsFocused == true)
{
ResetDataTable();
}
}
Since I don't display the timepicker in this control, I also make sure that it's the datetimepicker that has focus, and not the timepicker. Might be redundant
I solved this by comparing the original source and the source.
e.OriginalSource == e.Source
Related
I am trying to add UI elements dynamically, but I am facing a problem, I able to add UI elements, but I fail to add click event to Button
Below is my code :
ParserContext context = new ParserContext();
context.XmlnsDictionary.Add("", "http://schemas.microsoft.com/winfx/2006/xaml/presentation");
context.XmlnsDictionary.Add("x", "http://schemas.microsoft.com/winfx/2006/xaml");
string xaml = String.Format(" <StackPanel Orientation='Vertical'>");
xaml = xaml + "<StackPanel Orientation='Horizontal'>";
xaml = xaml + "<Button Margin='5,5,0,0' Background='AliceBlue' Foreground='DarkBlue' Height='25' VerticalAlignment='Bottom' HorizontalAlignment='Right' Width='82' Tag='12' Click='btnMy_Click'>";
xaml = xaml + "<StackPanel Orientation='Horizontal'>";
xaml = xaml + "<Image Source='/MotionTest;component/images/open.png' Width='18' Height='18' />";
xaml = xaml + "<TextBlock Text=' Open' VerticalAlignment='Center' FontSize='13' />";
xaml = xaml + "</StackPanel>";
xaml = xaml + "</Button>";
xaml = xaml + "</StackPanel>";
xaml = xaml + "</StackPanel>";
UIElement element = (UIElement)XamlReader.Parse(xaml, context);
myTestGrid.Children.Add(element);
And my onClick function :
private void btnMy_Click(object sender, RoutedEventArgs e)
{
var myValue = ((Button)sender).Tag;
MessageBox.Show("Here = " + myValue);
}
For this line :
xaml = xaml + "<Button Margin='5,5,0,0' Background='AliceBlue' Foreground='DarkBlue' Height='25' VerticalAlignment='Bottom' HorizontalAlignment='Right' Width='82' Tag='12' Click='btnMy_Click'>";
If I remove the
Click='btnMy_Click'
It will work. But if I add it, It shows
Anyone know how to solve this ?
Thanks in advance.
Since the error message states that to specify events, you need to compile your XAML file, and you specifically do not want to do that, it seems to be impractical (you may be able to compile a temporary assembly and load it, but then you can't unload it again).
What you could do is give the element a name:
xaml = xaml + "<Button x:Name='ClickMe'>";
And then, using a helper function to retrieve it:
var button = UIHelper.FindChild<Button>(element, "ClickMe");
button.Click += btnMy_Click;
The helper function looks like this, I took it from How can I find WPF controls by name or type? :
/// <summary>
/// Finds a Child of a given item in the visual tree.
/// </summary>
/// <param name="parent">A direct parent of the queried item.</param>
/// <typeparam name="T">The type of the queried item.</typeparam>
/// <param name="childName">x:Name or Name of child. </param>
/// <returns>The first parent item that matches the submitted type parameter.
/// If not matching item can be found,
/// a null parent is being returned.</returns>
public static T FindChild<T>(DependencyObject parent, string childName)
where T : DependencyObject
{
// Confirm parent and childName are valid.
if (parent == null) return null;
T foundChild = null;
int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < childrenCount; i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
// If the child is not of the request child type child
T childType = child as T;
if (childType == null)
{
// recursively drill down the tree
foundChild = FindChild<T>(child, childName);
// If the child is found, break so we do not overwrite the found child.
if (foundChild != null) break;
}
else if (!string.IsNullOrEmpty(childName))
{
var frameworkElement = child as FrameworkElement;
// If the child's name is set for search
if (frameworkElement != null && frameworkElement.Name == childName)
{
// if the child's name is of the request name
foundChild = (T)child;
break;
}
}
else
{
// child element found.
foundChild = (T)child;
break;
}
}
return foundChild;
}
The Setting:
I have a RichTextBox containing a hyperink and a DropDownButton somewhere else in my UI. Now when I click the button's DropDown open and afterwards click somewhere else on my UI, the DropDown is implemented to close, and check if it still owns the keyboardfocus so it can set its ToggleButton to focused again after the DropDown collapsed as intended.
The Problem:
When clicking inside my RichTextBox I will face an InvalidOperationException caused by my method to check focus ownership. The call to VisualTreeHelper.GetParent(potentialSubControl) works fine for all elements that are part of the VisualTree. Apparently the focused Hyperlink (returned by FocusManager.GetFocusedElement()) is not part of the VisualTree and therefore is invalid input to GetParent(). Well, how can I find the parent (either logical parent or visual parent) of a hyperlink within my RichTextBox?
My method for determining focus ownership:
// inside DropDownButton.cs
protected override void OnLostFocus( RoutedEventArgs e )
{
base.OnLostFocus( e );
if (CloseOnLostFocus && !DropDown.IsFocused()) CloseDropDown();
}
// inside static class ControlExtensions.cs
public static bool IsFocused( this UIElement control )
{
DependencyObject parent;
for (DependencyObject potentialSubControl =
FocusManager.GetFocusedElement() as DependencyObject;
potentialSubControl != null; potentialSubControl = parent)
{
if (object.ReferenceEquals( potentialSubControl, control )) return true;
try { parent = VisualTreeHelper.GetParent(potentialSubControl); }
catch (InvalidOperationException)
{
// can happen when potentialSubControl is technically
// not part of the visualTree
// for example when FocusManager.GetFocusedElement()
// returned a focused hyperlink (System.Windows.Documents.Hyperlink)
// from within a text area
parent = null;
}
if (parent == null) {
FrameworkElement element = potentialSubControl as FrameworkElement;
if (element != null) parent = element.Parent;
}
}
return false;
}
[Edit]
One potential idea to solve the issue: since Hyperlink is a DependencyObject I could try to access its inheritance context and find other DependencyObjects higher up in the tree and test them for being FrameworkElements. But I struggle to find any information about inheritance context in Silverlight.
I've been working on an application months ago, my friend and i wanted to share this application with other friends, and i need to display some help to make the application easier because it was designed only for both of us.
The idea that came out is to display help on a text Block every time a hover event is popped from a button. So we added a textBlock. Now the problem that we still facing is how to create the Hover Event for every button in our Main Window, there are a lots of buttons in this window, So we can't add an event to every button in the XAML code.
What i am expecting from this answer is a way to add Hover Event to all buttons in the main window Programmatically ?
EDIT: after some googling and help, i can do the following :
foreach (UIElement btn in GeneralMenuGrid.Children)
{
if (btn is Button)
{
Button currentButton = (Button)btn;
currentButton.Content = "test";
}
}
This is just a test that will allow all the buttons in the GeneralMenuGrid control to have a content : test, now the problem again is that i have nested controls in this grid, how can i reach them?
EDIT : after years of goggling i got to loop through all the buttons in my window with this :
public static void EnumVisuals(Visual argVisual, Window currentWindow)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(argVisual); i++)
{
Visual childVisual = (Visual) VisualTreeHelper.GetChild(argVisual, i);
if (childVisual is Button)
{
var button = childVisual as Button;
button.MouseEnter += AllButtonsOnMouseEnter;
}
EnumVisuals(childVisual, currentWindow);
}
}
now in the AllButtonsOnMouseEnter function, i can't access a button, i made it public... i can't access it from this class, how can i send the window with the event arguments?
You wrote, "there are a lots of buttons in this window, so we can't add an event to every button in the XAML code." But you can - just add a style that applies to all buttons:
<Style TargetType="{x:Type Button}">
<EventSetter Event="MouseEnter" Handler="Button_MouseEnter"/>
</Style>
I don't know how you intend to get the help text relevant to each Button, but it's easy to store it in the Button's Tag:
<Button Tag="Instantly move from one place to another.">
Teleport
</Button>
Then write an event handler that shows the help in your TextBlock:
private void Button_MouseEnter(object sender, MouseEventArgs e)
{
Button button = sender as Button;
textblock_that_shows_help.Text = button.Tag;
}
I've created an extension method that does this, adapted from here: Find all controls in WPF Window by type
Put this class somewhere in your project:
public static class VisualTreeSearch
{
/// <summary>
/// Finds all elements of the specified type in the <see cref="System.Windows.DependencyObject"/>'s visual tree using a breadth-first search.
/// </summary>
/// <typeparam name="T">The type of element to search for.</typeparam>
/// <param name="root">The object to search in.</param>
/// <returns>A list of elements that match the criteria.</returns>
public static IEnumerable<T> Find<T>(this DependencyObject root) where T : DependencyObject
{
return root.Find<T>(false, true);
}
/// <summary>
/// Finds all elements of the specified type in the <see cref="System.Windows.DependencyObject"/>'s visual tree.
/// </summary>
/// <typeparam name="T">The type of element to search for.</typeparam>
/// <param name="root">The object to search in.</param>
/// <param name="depthFirst">True to do a depth-first search; false to do a breadth-first search</param>
/// <param name="includeRoot">True to include the root element in the search; false to exclude it</param>
/// <returns>A list of elements that match the criteria.</returns>
public static IEnumerable<T> Find<T>(this DependencyObject root, bool depthFirst, bool includeRoot) where T : DependencyObject
{
if (includeRoot)
{
var depRoot = root as T;
if (depRoot != null)
yield return depRoot;
}
var searchObjects = new LinkedList<DependencyObject>();
searchObjects.AddFirst(root);
while (searchObjects.First != null)
{
var parent = searchObjects.First.Value;
var count = VisualTreeHelper.GetChildrenCount(parent);
var insertAfterNode = depthFirst ? searchObjects.First : searchObjects.Last;
for (int i = 0; i < count; i++)
{
DependencyObject child = VisualTreeHelper.GetChild(parent, i);
var depChild = child as T;
if (depChild != null)
yield return depChild;
insertAfterNode = searchObjects.AddAfter(insertAfterNode, child);
}
searchObjects.RemoveFirst();
}
}
}
To use the extension method, write the following in your window class (as an example). This code loops through all children, grandchildren, etc. of this (which should be your Window in this case) and finds all Button elements.
foreach (var button in this.Find<Button>())
{
button.MouseEnter += button_MouseEnter;
}
...
private void button_MouseEnter(object sender, MouseEventArgs e)
{
// Do stuff
}
after a lot of googling and chat help... i finally did it, maybe other will be interested this is how i proceeded :
i created a static recursive function that will get all the buttons in the window:
public static void EnumVisuals(Visual argVisual, Button toModifyButton)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(argVisual); i++)
{
Visual childVisual = (Visual) VisualTreeHelper.GetChild(argVisual, i);
if (childVisual is Button)
{
var button = childVisual as Button;
button.MouseEnter += (sender, eventArgs) =>
{
toModifyButton.Content = "Get the Help text from database if you want";
};
}
EnumVisuals(childVisual, toModifyButton);
}
}
why do you send a Button :
i need to write the help in a button, and the only way i found to access it's content property is to send it via this function and of course make it public.
Hope you'll find this helpfull.
I have a situation where I have several (let's say 10 for now) DevExpress LookUpEdit controls (sort of like DropDown Controls for Windows) on several tabs on my form. All 10 of these controls are the same in that they use the same data source to get their information from. What I'm trying to do is if a value changes in ANY of the 10 controls, then I want it to change in the other 9 controls as well. Because I have other controls of this type on my form that I do not want to be changed, I simply added a string value to the Tag property and have a method search for all controls of this type with the Tag property set to a specific string.
I initially thought I might be able to create a generic method that would change the text for all other controls and assign it to the TextChanged event of each control but I soon found out that they cancel each other out once I assign the value of one control to another (because in essence, once I change the value of one control, it then calls the same method and tries to change the rest).
Sorry if it's confusing, but here's some code as to what I'm trying to do...for now, let's say I have just 2 controls...lookupWeight and lookupBicycleWeight. On the TextChanged event for each, I have this:
private void OnLookupWeight_TextChanged(object sender, EventArgs e)
{
OnLookupWeight_TextChanged<LookUpEdit>(sender, e);
}
which calls this:
private void OnLookupWeight_TextChanged<T>(object sender, EventArgs e)
{
var controls = GetAll(tabPageSpecifications, typeof(T));
foreach (var control in controls)
{
if (control.Tag != null)
if (control.Tag.ToString() == "Weight")
if(control.Name != (sender as LookUpEdit).Name)
(control as LookUpEdit).EditValue = (sender as LookUpEdit).Text;
}
}
GetAll is a simple method that returns all controls for a given Control, including sub-controls:
/// <summary>
/// method to get all child & sub-child controls within a control by type
/// </summary>
/// <param name="control">the control we're searching in (use this for the form)</param>
/// <param name="type">The control type we're looking for (i.e; TextBox)</param>
/// <returns></returns>
public IEnumerable<Control> GetAll(Control control, Type type = null)
{
var controls = control.Controls.Cast<Control>();
//check the all value, if true then get all the controls
//otherwise get the controls of the specified type
if (type == null)
return controls.SelectMany(ctrl => GetAll(ctrl, type)).Concat(controls);
else
return controls.SelectMany(ctrl => GetAll(ctrl, type)).Concat(controls).Where(c => c.GetType() == type);
}
I know my OnLookupWeight_TextChanged method isn't entirely generic because I cast to the type LookupEdit but I am just trying to get this to work at this point before going back and changing things.
As you can see, the line if(control.Name != (sender as LookUpEdit).Name) is where the OnLookupWeight_TextChanged gets fired again and basically cancels itself out.
Any help or diretion as to how to accomplish this would be great.
Just change the ones that don't already have the same Text value:
foreach (var control in controls)
{
if (control.Tag != null)
if (control.Tag.ToString() == "Weight")
if((control as LookUpEdit).EditValue != (sender as LookUpEdit).Text)
(control as LookUpEdit).EditValue = (sender as LookUpEdit).Text;
}
It sounds like the code in OnLookupWeight_TextChanged shouldn't be run when the text of the control is changed from within that routine. You can use an instance variable to disable running it except when the change comes from outside. For example:
private Boolean InProgress;
private void OnLookupWeight_TextChanged<T>(object sender, EventArgs e)
{
if (!InProgress)
{
InProgress=true;
var controls = GetAll(tabPageSpecifications, typeof(T));
foreach (var control in controls)
{
if (control.Tag != null)
if (control.Tag.ToString() == "Weight")
if(control.Name != (sender as LookUpEdit).Name)
(control as LookUpEdit).EditValue = (sender as LookUpEdit).Text;
}
InProgress = false;
}
}
Why don't you use the same bindingsource for all the 10 controls?
When you change the selected value in one of them all would change.
My goal is to create a reusable Attached Behavior for a FlowDocumentScrollViewer, so that the viewer automaticly scrolls to the end whenever the FlowDocument has been updated (appended).
Problems so far:
OnEnabledChanged gets called before the visual tree is completed, and thus doesn't find the ScrollViewer
I don't know how to attach to the DependencyProperty containing the FlowDocument. My plan was to use it's changed event to initialize the ManagedRange property. (Manually triggered for the first time if needed.)
I don't know how to get to the ScrollViewer property from within the range_Changed method, as it doesn't have the DependencyObject.
I realize that those are potentially 3 separate issues (aka. questions). However they are dependent on each other and the overall design I've attempted for this behavior. I'm asking this as a single question in case I'm going about this the wrong way. If I am, what is the right way?
/// Attached Dependency Properties not shown here:
/// bool Enabled
/// DependencyProperty DocumentProperty
/// TextRange MonitoredRange
/// ScrollViewer ScrollViewer
public static void OnEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d == null || System.ComponentModel.DesignerProperties.GetIsInDesignMode(d))
return;
DependencyProperty documentProperty = null;
ScrollViewer scrollViewer = null;
if (e.NewValue is bool && (bool)e.NewValue)
{
// Using reflection so that this will work with similar types.
FieldInfo documentFieldInfo = d.GetType().GetFields().FirstOrDefault((m) => m.Name == "DocumentProperty");
documentProperty = documentFieldInfo.GetValue(d) as DependencyProperty;
// doesn't work. the visual tree hasn't been built yet
scrollViewer = FindScrollViewer(d);
}
if (documentProperty != d.GetValue(DocumentPropertyProperty) as DependencyProperty)
d.SetValue(DocumentPropertyProperty, documentProperty);
if (scrollViewer != d.GetValue(ScrollViewerProperty) as ScrollViewer)
d.SetValue(ScrollViewerProperty, scrollViewer);
}
private static ScrollViewer FindScrollViewer(DependencyObject obj)
{
do
{
if (VisualTreeHelper.GetChildrenCount(obj) > 0)
obj = VisualTreeHelper.GetChild(obj as Visual, 0);
else
return null;
}
while (!(obj is ScrollViewer));
return obj as ScrollViewer;
}
public static void OnDocumentPropertyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.OldValue != null)
{
DependencyProperty dp = e.OldValue as DependencyProperty;
// -= OnFlowDocumentChanged
}
if (e.NewValue != null)
{
DependencyProperty dp = e.NewValue as DependencyProperty;
// += OnFlowDocumentChanged
// dp.AddOwner(typeof(AutoScrollBehavior), new PropertyMetadata(OnFlowDocumentChanged));
// System.ArgumentException was unhandled by user code Message='AutoScrollBehavior'
// type must derive from DependencyObject.
}
}
public static void OnFlowDocumentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TextRange range = null;
if (e.NewValue != null)
{
FlowDocument doc = e.NewValue as FlowDocument;
if (doc != null)
range = new TextRange(doc.ContentStart, doc.ContentEnd);
}
if (range != d.GetValue(MonitoredRangeProperty) as TextRange)
d.SetValue(MonitoredRangeProperty, range);
}
public static void OnMonitoredRangeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.OldValue != null)
{
TextRange range = e.OldValue as TextRange;
if (range != null)
range.Changed -= new EventHandler(range_Changed);
}
if (e.NewValue != null)
{
TextRange range = e.NewValue as TextRange;
if (range != null)
range.Changed -= new EventHandler(range_Changed);
}
}
static void range_Changed(object sender, EventArgs e)
{
// need ScrollViewer!!
}
OnEnabledChanged gets called before
the visual tree is completed, and thus
doesn't find the ScrollViewer
Use Dispatcher.BeginInvoke to enqueue the rest of the work to happen asynchronously, after the visual tree is built. You will also need to call ApplyTemplate to ensure that the template has been instantiated:
d.Dispatcher.BeginInvoke(new Action(() =>
{
((FrameworkElement)d).ApplyTemplate();
d.SetValue(ScrollViewerProperty, FindScrollViewer(d));
}));
Note that you don't need to check whether the new value is different from the old one. The framework handles that for you when setting dependency properties.
You could also use FrameworkTemplate.FindName to get the ScrollViewer from the FlowDocumentScrollViewer. FlowDocumentScrollViewer has a named template part of type ScrollViewer called PART_ContentHost that is where it will actually host the content. This can be more accurate in case the viewer is re-templated and has more than one ScrollViewer as a child.
var control = d as Control;
if (control != null)
{
control.Dispatcher.BeginInvoke(new Action(() =>
{
control.ApplyTemplate();
control.SetValue(ScrollViewerProperty,
control.Template.FindName("PART_ContentHost", control)
as ScrollViewer);
}));
}
I don't know how to attach to the
DependencyProperty containing the
FlowDocument. My plan was to use it's
changed event to initialize the
ManagedRange property. (Manually
triggered for the first time if
needed.)
There is no way built into the framework to get property changed notification from an arbitrary dependency property. However, you can create your own DependencyProperty and just bind it to the one you want to watch. See Change Notification for Dependency Properties for more information.
Create a dependency property:
private static readonly DependencyProperty InternalDocumentProperty =
DependencyProperty.RegisterAttached(
"InternalDocument",
typeof(FlowDocument),
typeof(YourType),
new PropertyMetadata(OnFlowDocumentChanged));
And replace your reflection code in OnEnabledChanged with simply:
BindingOperations.SetBinding(d, InternalDocumentProperty,
new Binding("Document") { Source = d });
When the Document property of the FlowDocumentScrollViewer changes, the binding will update InternalDocument, and OnFlowDocumentChanged will be called.
I don't know how to get to the
ScrollViewer property from within the
range_Changed method, as it doesn't
have the DependencyObject.
The sender property will be a TextRange, so you could use ((TextRange)sender).Start.Parent to get a DependencyObject and then walk up the visual tree.
An easier method would be to use a lambda expression to capture the d variable in OnMonitoredRangeChanged by doing something like this:
range.Changed += (sender, args) => range_Changed(d);
And then creating an overload of range_Changed that takes in a DependencyObject. That will make it a little harder to remove the handler when you're done, though.
Also, although the answer to Detect FlowDocument Change and Scroll says that TextRange.Changed will work, I didn't see it actually fire when I tested it. If it doesn't work for you and you're willing to use reflection, there is a TextContainer.Changed event that does seem to fire:
var container = doc.GetType().GetProperty("TextContainer",
BindingFlags.Instance | BindingFlags.NonPublic).GetValue(doc, null);
var changedEvent = container.GetType().GetEvent("Changed",
BindingFlags.Instance | BindingFlags.NonPublic);
EventHandler handler = range_Changed;
var typedHandler = Delegate.CreateDelegate(changedEvent.EventHandlerType,
handler.Target, handler.Method);
changedEvent.GetAddMethod(true).Invoke(container, new object[] { typedHandler });
The sender parameter will be the TextContainer, and you can use reflection again to get back to the FlowDocument:
var document = sender.GetType().GetProperty("Parent",
BindingFlags.Instance | BindingFlags.NonPublic)
.GetValue(sender, null) as FlowDocument;
var viewer = document.Parent;
Does this help?
It's a good start at least (maybe?).