i could need some help on something i can't figure out at this point:
I need to update XAML-Bindings (via a class to varying resx-files) on a certain event for controls with a specific name prefix. As the controls have different types and i do no know how the same page will look somewhen in the future, i'd like to do that with reflections only...
something like
var meth1 = control.GetType().GetMethod("GetBindingExpression");
var meth2 = control.GetType().GetMethod("SetBinding");
BindingExpression be = (BindingExpression)meth1.Invoke(target, null);
Binding bind = be.ParentBinding;
meth2.Invoke(target, new object[] { bind });
seems to be the right idea to me, but i cannot figure out how to get the target DependencyProperty from a DependencyObject without knowing the DependencyObject Type before...
I am pretty sure i am missing something rather easy here ...
[edit]
I know that i can go through the controls and just enter the new string i get from a ResourceManager Object into the e.g. Text-Property of the Control, but in that case i'd again have to check for Text, Header, whatever properties ... if possible, reflections only seem the cleaner way to me.
You can simply use following way,
FrameworkElement fe = control as FrameworkElement;
foreach(PropertyDescriptor pd in TypeDescriptor.GetProperties(control))
{
FieldInfo field = control.GetType().GetField(pd.Name + "Property");
if(field == null)
continue;
DependencyProperty dp = field.GetValue(control) as DependencyProperty;
if(dp == null)
continue;
BindingExpression be = control.GetBindingExpression(dp);
if(be == null)
continue;
// do your stuff here
}
Thanks to the both hints regarding the cast to FrameworkElement, i managed to get back on track:
foreach (var f in control.GetType().GetFields())
{
DependencyProperty dp = f.GetValue(control) as DependencyProperty;
if (dp != null)
{
BindingExpression be = ((FrameworkElement)control).GetBindingExpression(dp);
if (be != null)
{
// stuff here
}
}
}
From here on i think will get things done
Related
I am working on a windows phone app. I want to copy children of one canvas to other canvas. I can do it with the following code but the problem is I have to remove it from one canvas first. Code is:
private void add_template_Click(object sender, RoutedEventArgs e)
{
var childrenList = Template_canvas1.Children.Cast<UIElement>().ToArray();
root.Children.Clear();
foreach (var c in childrenList)
{
Template_canvas1.Children.Remove(c);
root.Children.Add(c);
}
}
I want to keep these elements on both the canvas. Is there another way?
Instead of trying to add the same Template_canvas1.Children to the root canvas, first make a copy of those Children and then add the copy to the root canvas.
public static T CloneXaml<T>(T source)
{
string xaml = XamlWriter.Save(source);
StringReader sr = new StringReader(xaml);
XmlReader xr = XmlReader.Create(sr);
return (T)XamlReader.Load(xr);
}
Then change your loop to:
foreach (var c in childrenList)
{
var copy = CloneXaml(c);
root.Children.Add(copy);
}
I haven't tested this code, so you may have to modify it a bit, but it should put you in the right direction.
Alternatively, you can probably use the code below which is copied from Dr Herbie's answer:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.UI.Xaml;
using System.Reflection;
using Windows.UI.Xaml.Controls;
namespace UIElementClone {
public static class UIElementExtensions {
public static T DeepClone<T>(this T source) where T : UIElement {
T result; // Get the type
Type type = source.GetType(); // Create an instance
result = Activator.CreateInstance(type) as T;
CopyProperties<T>(source, result, type);
DeepCopyChildren<T>(source, result);
return result;
}
private static void DeepCopyChildren<T>(T source, T result) where T : UIElement {
// Deep copy children.
Panel sourcePanel = source as Panel;
if (sourcePanel != null) {
Panel resultPanel = result as Panel;
if (resultPanel != null) {
foreach (UIElement child in sourcePanel.Children) {
// RECURSION!
UIElement childClone = DeepClone(child);
resultPanel.Children.Add(childClone);
}
}
}
}
private static void CopyProperties<T>(T source, T result, Type type) where T : UIElement {
// Copy all properties.
IEnumerable<PropertyInfo> properties = type.GetRuntimeProperties();
foreach (var property in properties) {
if (property.Name != "Name") { // do not copy names or we cannot add the clone to the same parent as the original.
if ((property.CanWrite) && (property.CanRead)) {
object sourceProperty = property.GetValue(source);
UIElement element = sourceProperty as UIElement;
if (element != null) {
UIElement propertyClone = element.DeepClone();
property.SetValue(result, propertyClone);
}
else {
try {
property.SetValue(result, sourceProperty);
}
catch (Exception ex) {
System.Diagnostics.Debug.WriteLine(ex);
}
}
}
}
}
}
}
}
If none of these worked for you, I'm afraid you'd have to implement your own serializer. It looks like David Poll implemented a decent serlizer, so have a look. Using his serlizer is as simple as using the XamlWriter, then you can use the XamlReader:
public static T CloneXaml<T>(T source)
{
UiXamlSerializer uxs = new UiXamlSerializer();
string xaml = uxs.Serialize(source);
StringReader sr = new StringReader(xaml);
XmlReader xr = XmlReader.Create(sr);
return (T)XamlReader.Load(xr);
}
To get this functionality, download his Slab library, go to the "Binaries" folder and copy all the dlls that start with "SLaB.Utilities.Xaml.Serializer" to your project. There might be some other dlls required as dependency. He has example solution in the library if you like to look at the code an learn.
Without a good Minimal, Complete, and Verifiable example that shows clearly what you've tried, with a precise description of what exactly you're trying to achieve, it's impossible to know for sure what the best answer would be.
That said, given that WPF already knows how to "clone" elements in a sense, through the use of data templates, your question really sounds a lot like an XY Problem to me. That is, you only think you need to literally clone the elements already in your visual tree, when in fact what you should be doing is defining a view model that represents the data to be displayed for the element(s) to be "cloned", define a single data template that uses XAML to describe the visual elements that will display the data in the view model, and then simply apply the template as necessary wherever you want the visual elements to be "cloned".
I.e. they won't really be cloned. Instead, WPF will automatically populate a whole new sub-tree of visual elements exactly as you want them to be. Since the template allows you to completely define all aspects, there is no issue related to e.g. trying to get the event subscriptions hooked up, setting up bindings correctly, etc.
In your specific example (vague though it is), it sounds like you most likely want to use an ItemsControl element, in which the ItemsPanel is a Canvas object. You would then define a DataTemplate that represents a single item in the ItemsPanel; this template would be referenced either implicitly by setting its DataType property, or explicitly by setting the ItemsControl.ItemTemplate property. Then, instead of cloning anything, you just create an ItemsControl when you want a copy of your visual for the data.
New answer after user's feedback that it is not working on Windows Phone
Complete final Windows Phone App can be downloaded here.
There are some API differences, for example instead of pinfo.SetMethod property we have to use pinfo.GetSetMethod() etc.
Secondly, I was unknowingly didn't check for Name property which must not be copied as otherwise we would be making another instance with same name.
Third, I posted for simple case of simple controls like Button, TextBox, Rectangle etc which do not contain children. If that is the case you have to go for recursive deep cloning to clone children too. As children could have more children and so on.
foreach (UIElement oldElem in Canvas1.Children)
{
try
{
Type t = oldElem.GetType();
UIElement newElem = (UIElement)Activator.CreateInstance(t);
PropertyInfo[] info = t.GetProperties();
int i = 0;
foreach (PropertyInfo pinfo in info)
{
if (pinfo.Name == "Name") continue;
try
{
if (pinfo.GetSetMethod() != null) // avoid read-only properties
pinfo.SetValue(newElem, pinfo.GetValue(oldElem, null),null);
}
catch (Exception ex)
{
Debug.WriteLine((++i).ToString() + " : " + pinfo.ToString());
}
}
Canvas.SetLeft(newElem, Canvas.GetLeft((oldElem)));
Canvas.SetTop(newElem, Canvas.GetTop((oldElem)));
Canvas2.Children.Add(newElem);
}
catch (Exception ex)
{
}
}
And if you are going for truly deep cloning then replace code in outer try block above with a simpler:
foreach (UIElement oldElem in Canvas1.Children)
{
try
{
UIElement newElem = oldElem.DeepClone();
Canvas2.Children.Add(newElem);
Canvas.SetLeft(newElem, Canvas.GetLeft(oldElem));
Canvas.SetTop(newElem, Canvas.GetTop(oldElem));
}
catch (Exception ex){ }
}
Old answer based on WPF only
Don't know about windows phone but in WPF this creates a fresh element and puts it in exactly same place in another canvas. Check if it fits your needs, else I will update it again.
foreach (UIElement oldElem in Canvas1.Children)
{
Type t = oldElem.GetType();
UIElement newElem = (UIElement)Activator.CreateInstance(t);
PropertyInfo[] info = t.GetProperties();
int i = 0;
foreach (PropertyInfo pinfo in info)
{
try
{
if (pinfo.SetMethod != null) // avoid read-only properties
pinfo.SetValue(newElem, pinfo.GetValue(oldElem));
}
catch (Exception ex)
{
Debug.WriteLine((++i).ToString() + " : " + pinfo.ToString());
}
}
Canvas.SetLeft(newElem, Canvas.GetLeft((oldElem)));
Canvas.SetTop(newElem, Canvas.GetTop((oldElem)));
Canvas.SetRight(newElem, Canvas.GetRight((oldElem)));
Canvas.SetBottom(newElem, Canvas.GetBottom((oldElem)));
Canvas2.Children.Add(newElem);
}
This is XAML code;
<toolkit:AutoCompleteBox x:Name="newTaskNameTextBox"
ItemsSource="{StaticResource BankNamesList}" />
How to assign this ItemSource attribute to the newTaskNameTextBox by C# programmatically ?
(Solution for WPF)
You should use the TryFindResource method.
newTaskNameTextBox.ItemsSource =
(IEnumerable)newTaskNameTextBox.TryFindResource("BankNamesList");
This searches up the logical tree, in the same way {StaticResource BankNamesList} does.
UPDATE: (solution for WP8)
Sounds lile you're using WP8 (which doesn't include FindResource / TryFindResource) so try this instead:
newTaskNameTextBox.ItemsSource = (IEnumerable)Resources["BankNamesList"];
UPDATE: (how to implement the missing TryFindResource)
Note that the code above requires the resource to exist in the owner of this code behind (e.g. the window). However, there may be cases where the resource exists in another parent element up the logical tree. For example, you may be writing the code behind for a custom user control but the resource you're looking for exists in the MainWindow. For such cases, it wouldn't be too hard to write a basic implementation of WPF's TryFindResouces, which has the advantage of searching up the logical tree (source link):
public static class FrameworkElementExtensions
{
public static object TryFindResource(this FrameworkElement element, object resourceKey)
{
var currentElement = element;
while (currentElement != null)
{
var resource = currentElement.Resources[resourceKey];
if (resource != null)
{
return resource;
}
currentElement = currentElement.Parent as FrameworkElement;
}
return Application.Current.Resources[resourceKey];
}
}
/**********************************************************************/
// Or, the recursive version of TryFindResource method as suggested by #Default:
public static object TryFindResource(this FrameworkElement element, object resourceKey)
{
if (element == null)
return Application.Current.Resources[resourceKey];
var resource = element.Resources[resourceKey];
if (resource != null)
{
return resource;
}
return TryFindResource(element.Parent, resourceKey);
}
So if you include this FrameworkElementExtensions class in your namespace, then you should be able to do this (same code I've given for WPF originally):
newTaskNameTextBox.ItemsSource =
(IEnumerable)newTaskNameTextBox.TryFindResource("BankNamesList");
If BankNamesList is resource in the resources of your window, then in code behind you can do:
newTaskNameTextBox.ItemsSource = Resources["BankNamesList"]
Try this:
newTaskNameTextBox.ItemsSource = (IEnumerable)(Application.Current.Resources["BankNamesList"]);
I have a silverlight app consisting of several dialogs each with a collection of FrameworkElements in it.
Is it possible to find the dialog in which a Framework element is in?
You can use the VisualTreeHelper. The code below is what I use to find the Page in a WPF application. You can replace Page with whatever container you need in Silverlight, maybe Popup.
var parent = VisualTreeHelper.GetParent(this);
while (!(parent is Page))
{
parent = VisualTreeHelper.GetParent(parent);
}
http://forums.silverlight.net/p/55369/142519.aspx has a method to simplify the above example code and make it generic-friendly:
public static class ControlFinder
{
public static T FindParent<T>(this UIElement control) where T: UIElement
{
UIElement p = VisualTreeHelper.GetParent(control) as UIElement;
if (p != null)
{
if (p is T)
return p as T;
else
return ControlFinder.FindParent<T>(p);
}
return null;
}
}
Use it like:
var page = myElement.FindParent<Page>();
Yes it is possible. If you know the structure of your control, then you can user FrameworkElement.GetParent() or else you can use Tree-traversal algorithms like BFS or DFS to find the your framework element.
For example, I have a DataGridView control with a Blue BackgroundColor property etc.., is there a way which I can transfer or pass programatically these properties to another DataGridView control?
Something like this:
dtGrid2.Property = dtGrid1.Property; // but of course, this code is not working
Thanks...
You'll need to use reflection.
You grab a reference to each property in your source control (based on its type), then "get" its value - assigning that value to your target control.
Here's a crude example:
private void copyControl(Control sourceControl, Control targetControl)
{
// make sure these are the same
if (sourceControl.GetType() != targetControl.GetType())
{
throw new Exception("Incorrect control types");
}
foreach (PropertyInfo sourceProperty in sourceControl.GetType().GetProperties())
{
object newValue = sourceProperty.GetValue(sourceControl, null);
MethodInfo mi = sourceProperty.GetSetMethod(true);
if (mi != null)
{
sourceProperty.SetValue(targetControl, newValue, null);
}
}
}
You could use reflection to get all the public properties of the type and copy the values from one instance to another, but this is dangerous and might not really duplicate the entire state of the object. There might be some properties that you don't want to copy (e.g. Parent, Site), and other important properties that you can't set directly (e.g. Columns, Rows). Also, there could be properties that are reference types; your copied control would end up referencing the same object as your original, which could be undesirable. There could also be state information that can only be set through method calls, which won't be copied this way. In short, reflection probably isn't the solution you're looking for.
You may just have to manually copy the properties you want. Alternatively, you could create a factory method that can create any number of similar grids.
I posted a demo project on codeproject on how to copy&paste or clone a contorl a few years ago,
http://www.codeproject.com/Articles/12976/How-to-Clone-Serialize-Copy-Paste-a-Windows-Forms
Here's the code that I came up with. I've only tested it with the Label, TextBox, Panel, and DataGridView controls. For a Panel control you will get all the contained controls (cloned instances). For a DataGridView control you will get the data binding and it will be the exact same data that is bound to the source DataGridView control. Of course, if there is not binding then the cloned instance will have no binding. Whether these behaviors are desirable or not depends on your needs.
private Control CloneControl(Control srcCtl)
{
var cloned = Activator.CreateInstance(srcCtl.GetType()) as Control;
var binding = BindingFlags.Public | BindingFlags.Instance;
foreach(PropertyInfo prop in srcCtl.GetType().GetProperties(binding))
{
if (IsClonable(prop))
{
object val = prop.GetValue(srcCtl);
prop.SetValue(cloned, val, null);
}
}
foreach(Control ctl in srcCtl.Controls)
{
cloned.Controls.Add(CloneControl(ctl));
}
return cloned;
}
private bool IsClonable(PropertyInfo prop)
{
var browsableAttr = prop.GetCustomAttribute(typeof(BrowsableAttribute), true) as BrowsableAttribute;
var editorBrowsableAttr = prop.GetCustomAttribute(typeof(EditorBrowsableAttribute), true) as EditorBrowsableAttribute;
return prop.CanWrite
&& (browsableAttr == null || browsableAttr.Browsable == true)
&& (editorBrowsableAttr == null || editorBrowsableAttr.State != EditorBrowsableState.Advanced);
}
Based on this post here is a version that
creates the correct control types and
does so recursively
public static class ControlExtensions
{
public static T Clone<T>(this T controlToClone) where T : Control
{
PropertyInfo[] controlProperties =
typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
//T instance = Activator.CreateInstance<T>();
Control instance = (Control) Activator.CreateInstance(controlToClone.GetType());
foreach (PropertyInfo propInfo in controlProperties)
{
if (propInfo.CanWrite)
{
if (propInfo.Name != "WindowTarget")
propInfo.SetValue(instance,
propInfo.GetValue(controlToClone, null), null);
}
}
foreach(Control ctl in controlToClone.Controls)
{
instance.Controls.Add( ctl.Clone() );
}
return (T) instance;
}
}
You still may want to test if more than the WindowTarget property should be filtered out..
Funny aside: If the control to clone is (on) an unselected TabPage it will be invisible..
i used this:
Control NewControl=new Control(ControlToClone,ControlToClone.Name);
I Used below code to copy selected properties.
public static void CloneControl(Control SourceControl, Control DestinationControl)
{
String[] PropertiesToClone = new String[] { "Size", "Font", "Text", "Tag", "BackColor", "BorderStyle", "TextAlign", "Width", "Margin" };
PropertyInfo[] controlProperties = SourceControl.GetType().GetProperties();
foreach (String Property in PropertiesToClone)
{
PropertyInfo ObjPropertyInfo = controlProperties.First(a => a.Name == Property);
ObjPropertyInfo.SetValue(DestinationControl, ObjPropertyInfo.GetValue(SourceControl));
}
}
After having set all the properties in the Designer I then wanted to copy all this configurations to another DataGridView and I faced the same problem.
I solved it inspecting the InitializeComponent(); part of the form.
There was easy to find all the properies that I had set and to set it again by code to another DataGridView control.
For example, I had set:
DataGridViewCellStyle dgvCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle();
dgvCellStyle1.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft;
dgvCellStyle1.BackColor = System.Drawing.SystemColors.Window;
dgvCellStyle1.Font = new System.Drawing.Font("Consolas", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
dgvCellStyle1.ForeColor = System.Drawing.SystemColors.ControlText;
dgvCellStyle1.SelectionBackColor = System.Drawing.SystemColors.Highlight;
dgvCellStyle1.SelectionForeColor = System.Drawing.SystemColors.HighlightText;
dgvCellStyle1.WrapMode = System.Windows.Forms.DataGridViewTriState.False;
dgv.DefaultCellStyle = dgvCellStyle1;
dgv.GridColor = System.Drawing.SystemColors.ControlLight;
dgv.Location = new System.Drawing.Point(258, 315);
dgv.ReadOnly = true;
dgv.RowHeadersVisible = false;
dgv.RowTemplate.Height = 18;
dgv.ShowEditingIcon = false;
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?).