intercept RelativeSource FindAncestor - wpf

I have a WPF application which runs as a excel plugin, it has its visual tree like so
Excel
ElementHost
WPF UserControl
WPF ribbon bar control
Now any controls sitting on the WPF ribbon bar control are not enabled when the plugin is loaded within excel. See error below
System.Windows.Data Error: 4 : Cannot find source for binding with
reference 'RelativeSource FindAncestor, AncestorType='System.Windows.Window', AncestorLevel='1''. BindingExpression:Path=IsActive; DataItem=null; target element
is 'Ribbon' (Name=''); target property is 'NoTarget' (type 'Object')
If I nest the ribbon bar control in a standalone Window(outside excel) it works fine.
Is there a way to intercept the FindAncestor call for a Window and wire it to something else.? Note that I cannot change the above binding as it isn't my control.

The most direct answer
FindAncestor is processed internally by WPF and will search up the visual tree as far as it can before going anywhere else. Only when it reaches a Visual that has no visual parent will it search elsewhere, and this depends on what it reached. For example, if it hits a FrameworkContentElement it can go to the document's container. Unfortunately if the top of the visual tree is a ElementHost, it will stop, so there is no way to reroute the call.
This means that your simplest option is to replace the binding. Fortunately this is not very difficult.
How to automatically replace a binding
Here is a simple method I wrote a while back that searches through a visual tree and replaces bindings as directed by an updateFunction. If the updateFunction returns a different binding than it is passed, the binding is updated.
static void UpdateBindings(Visual visual, Func<Binding, Binding> updateFunction)
{
if(visual==null) return;
for(int i=0; i<VisualTreeHelper.GetChildrenCount(visual); i++)
UpdateBindings(VisualTreeHelper.GetChild(visual, i) as Visual, updateFunction);
for(var enumerator = visual.GetLocalValueEnumerator(); enumerator.MoveNext(); )
{
var property = enumerator.Current.Property;
var binding = BindingOperations.GetBinding(visual, property);
if(binding==null) continue;
var newBinding = updateFunction(binding);
if(newBinding!=binding)
BindingOperations.SetBinding(visual, property, newBinding);
}
}
To illustrate how this works, here is how you could write a method that replaces a specific AncestorType in all RelativeSource FindAncestor instances, as follows:
static void ReplaceFindAncestorType(Visual visual, Type fromType, Type toType)
{
UpdateBindings(visual, binding =>
binding.RelativeSource.Mode != RelativeSourceMode.FindAncestor ? binding :
binding.RelativeSource.AncestorType != fromType ? binding :
new Binding
{
RelativeSource = new RelativeSource(
RelativeSourceMode.FindAncestor,
toType,
binding.RelativeSource.AncestorLevel),
Path = binding.Path,
Mode = binding.Mode,
Converter = binding.Converter,
StringFormat = binding.StringFormat,
UpdateSourceTrigger = binding.UpdateSourceTrigger,
});
}
Note that only commonly-used properties are copied over to the new binding.
The ReplaceFindAncestorVisualType method could be used something like this:
elementHost.LayoutUpdated += (obj, e) =>
{
ReplaceFindAncestorType(elementHost, typeof(Window), typeof(ElementHost);
};
In your case this generic replace technique won't work: It will be looking for an IsActive property on your ElementHost, which does not exist. So you probably need to change more than just the RelativeSource. This means your actual code will be more like this:
elementHost.LayoutUpdated += (obj, e) =>
{
UpdateBindings(elementHost, binding =>
binding.RelativeSource.AncestorType != typeof(Window) ? binding :
new Binding
{
Source = ultimateContainingWindowOrOtherObjectHavingIsActiveProperty,
Path = new PropertyPath("IsActive"), // Put property name here
});
};
Note that the above code assumes any FindAncestor:Window binding is the one we are looking for. More conditions can be added as needed in the conditional.
Alternative solution
There is another, completely different, solution available: It is possible to actually host the content in a borderless Window and add custom code to keep this window positioned over the ElementHost so it appears to be within the other window. This is trickier than it sounds since you have to deal with things such as ActiveWindow, ForegroundWindow, Z Order, Minimized state, keyboard focus, etc. But if your needs are very simple this can be a reasonable solution.

When using the control in Excel there is no Window in the ancestry, however, perhaps you can use Snoop to find where the binding is defined, then during run-time, find the dependency object (by type) and change its property's binding expression?

Another option would be to add a custom control that inherits from Window as an ancestor, then bind that to the Excel control.

Related

WPF Datagrid Button commandParameter in code behind

I'm trying to build a datagrid with columns and a button in code behind and I want to assign the command parameter of this button to the value of the "ID" column which is "TDENT_ID". Here's my code
this line is not working as you can see :
BtnDetail.SetBinding(System.Windows.Controls.Primitives.ButtonBase.CommandParameterProperty, new Binding() { Source = dtGrid, Path = "TDENT_ID" });
How can I write it?
FrameworkElementFactory:
"This class is a deprecated way to programmatically create templates, which are subclasses of FrameworkTemplate such as ControlTemplate or DataTemplate; not all of the template functionality is available when you create a template using this class. The recommended way to programmatically create a template is to load XAML from a string or a memory stream using the Load method of the XamlReader class." Microsoft Docs
Your Bindingis wrong. It currently points to the DataGrid which does not have a property TDENT_ID.
This property is on your data item, which is the DataContext of the column's template.
Therefore, correct Binding would be:
// Set up a Binding that uses the current DataContext as Binding.Source
BtnDetail.SetBinding(ButtonBase.CommandParameterProperty, new Binding(nameof(myItemType.TDENT_ID)));
Microsoft Docs: Specifying the binding source
Try to learn XAML. It makes writing such UI related code much easier, which makes you more productive.

Can custom UIElement implement IList<UIElement> and be assigned children directly (in XAML)?

Scenario: I have a range of custom UIElements (in fact, I have replaced all the standard WPF FrameworkElements I would use with lighter, more efficient counterparts) for a custom layout system intended to only use those. They all inherit from a class called Surface (which in turn is a direct descendant of UIElement).
I am now wondering if my version of Panel (let's call it SurfacePanel) can simply implement IList<Surface> and allow child Surface elements to be added directly to it, rather than to a Children property (as with regular WPF panels), in XAML.
To illustrate - in codebehind, I can do now this:
SurfacePanel.Add(child);
And from that, I would like to be able to do this in XAML:
<SurfacePanel>
<child />
</SurfacePanel>
But XAML seems to require me to have a codebehind pattern like this:
SurfacePanel.Children.Add(child)
(I don't really need these controls to support XAML to work in the runtime environment, but when testing and prototyping, I like to make my UI controls "XAML friendly" so I can benefit from the visual designer in VS (along with the property pane etc), if nothing more than as a 'preview' window).
Since my controls inherit from UIElement (and have the proper Measure/Arrange/Render overrides and so on), they function quite well when put on, say, a regular Canvasor Grid. But the VS XAML parser is not too happy about my SurfacePanel (that implements IList<Surface>) when I am adding children to it in markup. It says "Cannot add content to an object of type "SurfacePanel"".
I know that if I add a Children property of an appropriate type and add an attribute to the SurfaceCanvas class ([ContentProperty("Children")]), it will work. But since the SurfacePanel is itself a collection capable of the same thing, is there a way to make XAML 'get it'?
Edit:
I can solve the XAML 'compliance' by adding a Children property on the SurfacePanel that simply returns its inner List, but then adding and removal of elements on that directly bypasses the internal logic that wire the child elements up.
If the inner list was an ObservableCollection, I could do it the conventional way and do the wiring in a CollectionChanged event handler - but basically the whole point of integrating IList in the Panel directly is to avoid that..
Edit 2:
This "works" (but bypasses the wiring):
[ContentProperty("Children")]
public class SurfacePanel : Surface, IList<Surface>
{
private readonly List<Surface> _children = new List<Surface>();
public List<Surface> Children
{
get { return _children; }
}
}
I cannot return this because SurfacePanel is not a List<Surface>, but an IList<Surface>.
If I change the property to
public IList<Surface> Children
{
get { return this; }
}
I get an error message even with the following XAML (but not with <m:SurfacePanel/>):
<m:SurfacePanel>
</m:SurfacePanel>
The error message is
Cannot set content property 'Children' on element 'SurfacePanel'. 'Children' has incorrect access level or its assembly does not allow access.
Also implement IList and declare the Children property like this:
[ContentProperty("Children")]
public class SurfacePanel : Surface, IList, IList<Surface>
{
public IList Children
{
get { return this; }
}
...
}

Find control from LayoutRoot in Silverlight

I have a multiple textblocks on my usercontrol Layoutroot the problem is how can I find a particular TextBlock by its name?
Thanx
var myElement =
((FrameworkElement)System.Windows.Application.Current.RootVisual)
.FindName("TextBlockName");
should work in this case, if the textblock has already been rendered.
To be able to easily traverse the visual tree more generally like #ColinE mentioned, you can also use the Silverlight toolkit.
// requires System.Windows.Controls.Toolkit.dll
using System.Windows.Controls.Primitives;
var myElement = myRoot.GetVisualDescendants().OfType<TextBlock>()
.Where(txb => txb.Name == "TextBlockName").FirstOrDefault();
If you are creating a UserControl, any element that you name via x:Name should be available to you as a field in your code-behind.
If you are not creating a UserControl, you can search the visual tree via Linq to VisualTree ...
TextBlock block = LayoutRoot.Descendants<TextBlock>()
.Cast<TextBlock>()
.SingleOrDefault(t => t.Name == "TextBlockName");
hey Masn i was write some code and similiar conditions in my case that all ok.
this is the case (have many listbox and named variables diferenciated by number in final the name Example: listAttachment1,listAttachment2,listAttachment3,..,etc). To best explication show my code:
public void refreshAttachmentList(ListlistOfControlsRequest, int identifier)
{
string valueName = "attachmentsField_"+identifier;
var mylistAttachment = ((FrameworkElement)System.Windows.Application.Current.RootVisual).FindName(valueName);
ListBox listAttachRequest = mylistAttachment as ListBox;
listAttachRequest.ClearValue(ItemsControl.ItemsSourceProperty);
listAttachRequest.ItemsSource = listOfAttachmentsControls;
listAttachRequest.....all properties
}

Rebinding all properties of all elements in the visual tree?

I know that I can rebind all instances of a specific property for a specific type of element, as in this method that rebinds the Text property of all Textblocks.
public void Rebind()
{
foreach (var textBlock in LayoutRoot.GetDescendents().OfType<TextBlock>())
{
BindingExpression bindExp = textBlock.GetBindingExpression(TextBlock.TextProperty);
if (bindExp != null)
{
Binding bind = bindExp.ParentBinding;
textBlock.SetBinding(TextBlock.TextProperty, bind);
}
}
}
What I want to be able to do though is rebind all properties that have bindings for all elements in the visual tree. More specifically I would like to rebind all bindings that use a specific value converter. How might I do so?
This isn't realistically acheivable since FrameworkElement provides no way to enumerate the set of binding expressions that currently apply to it.
In order to achieve this you would need to have first collected all the dependency properties that may apply (at least at a per element type but that adds further complications) and then attempt GetBindingExpression on each per element. Real ugly and real slow.
Time to design this requirement out.

How to Programmatically Modify a DataTemplate?

I'm trying to programmatically add events and elements to a DataTemplate in a Silverlight 3.0 app. I have a User Control with a dependency property where I would like to take the template that's set, tweak it, and then set the modified version to an inner control.
The idea I have is to take the DataTemplate that comes in, read its XAML, tweak it, and then use the XamlReader to create a modified DataTemplate that can then be set to the inner control. The issue with this approach is I don't know how to get the XAML from the originalal template (if it's even possible.) For example:
protected virtual void OnItemTemplateChanged(DependencyPropertyChangedEventArgs e)
{
// Get the original Xaml from the set template
//string originalXaml = ???
// Modify the template
string newXaml = originalXaml.Replace("foo", "bar"); // for example
// Create a new template from the modified XAML
DataTemplate newTemplate = (DataTemplate)XamlReader.Load(newXaml);
// Update the inner template
this._childDropdown.ItemTemplate = newTemplate;
}
Does someone know either: 1) if there's a way to read the original XAML, or 2) another approach to programmatically modify the DataTemplate.
Thanks,
You cannot manipulate the template via code (see documentation for FrameworkTemplate). The closest you are going to get is to call the DataTemplate's LoadContent to create an instance of the contained Xaml but you can't use that to manipulate the contents and there is no way inside Silverlight to convert a UIElement back to Xaml again.
The nearest I think you can get is to make your dependency object a Uri pointing to a Xaml resource that contains the initial DataTemplate.
You can then load this resource into an XDocument and manipulate it as XML. Subsequently you can use XamlReader to instance the DataTemplate and assign it to ItemTemplate.

Resources