c# - Binding to a ContentPresenter's visual elements/children from outside -
first brief "abstracted" short version of problem. not needed discuss solution, below further "optional" infos of real underlying problem i'm having, understand context.
so: have contentpresenter using datatemplate generate layout bound items. now, outside of contentpresenter, i'm trying bind within element name within content presenter.
assume following pseudo-xaml (maintextblock's binding won't work in practice):
<textblock text="{binding text, elementname=mytextblock, source = ???}" datacontext="{x:reference thecontentpresenter}" x:name="maintextblock"/> <contentpresenter content="{binding someitem}" x:name="thecontentpresenter"> <contentpresenter.contenttemplate> <datatemplate> <textblock x:name="mytextblock" text="test"/> </datatemplate> </contentpresenter.contenttemplate> </contentpresenter>
!! please assume datacontext of maintextblock must (a reference to) thecontentpresenter !!
given assumption, how can make binding on maintextblock work?
i can't bind contentpresenter's content property, because contains bound element (e.g. someitem), not visual representation. unfortunately, contentpresenter doesn't seem have property representing visual tree / visual children.
is there way this?
now need for? feel free skip reading this, shouldn't needed discuss solution of above's problem believe.
i'm writing behavior adds customizable filters datagrid:
<datagrid autogeneratecolumns="false"> <i:interaction.behaviors> <filter:filterbehavior> <filter:stringfilter column="{x:reference firstcol}" binding="{binding datacontext.value1}"/> <filter:stringfilter column="{x:reference secondcol}" binding="{??????? bind content -> visual children -> element name "mytextblock" -> property "text"}"/> </filter:filterbehavior> </i:interaction.behaviors> <datagrid.columns> <datagridtextcolumn x:name="firstcol" header="test" binding="{binding value1}"/> <datagridtemplatecolumn x:name="secondcol" header="test 2"> <datagridtemplatecolumn.celltemplate> <datatemplate> <textblock x:name="mytextblock" text="{binding value2}"/> </datatemplate> </datagridtemplatecolumn.celltemplate> </datagridtemplatecolumn> </datagrid.columns> </datagrid>
"filterbehavior" contains individual filters each column, e.g. first of them filter allows search text within whatever column bound (firstcol in case), , hides columns text doesn't appear.
now, binding interesting part. binding property of type bindingbase (so binding "deferred"). it's intended define value used filtering. when filtering supposed occur, each filter loops through datagridcells of column bound to. each datagridcell, sets binding's datacontext respective datagridcell, , evaluates binding.
so, stringfilter loop through each datagridcell in firstcol. each of them, retreive bindingbase "binding" (i.e. {binding datacontext.value1}), set datacontext datagridcell, , evaluate this. in case, bind wpfgridcell.datacontext.value1, or in other words value1 property of item datagridcell contains. later on, check if these evaluated items match string user entered filtering.
this works fine.
however, i'm having trouble when trying bind datagridcell's visual content, in case of second stringfilter column="{x:reference secondcol}". secondcol datagridtemplatecolumn. it's cells content contentpresenter, template datagridtemplatecolumn.celltemplate, , content element cell contains.
and simplified version above. need evaluate "binding" datacontext = datagridcell, , somehow come binding let's me bind visual elements of contentpresenter given in datagridcell.content.
thanks!
since no other solution came far / doesn't appear possible xaml only, here's current solution. seems bit messy, works , allows relatively general use.
essentially, i've introduced second property filters called "bindingcontext", of type bindingbase. consumer can either leave @ null, in case it'll default respective datagridcell, or can assign binding (which datacontext = datagridcell). binding evaluated, , result of used datacontext "binding" property:
<filter:stringfilter column="{x:reference secondcol}" bindingcontext="{binding content, converter={staticresource contentpresentertovisualhelperconverter}, converterparameter='mytextblock'}" binding="{binding visual.text, mode=oneway, updatesourcetrigger=propertychanged}" />
now i've created ivalueconverter converts contentpresenter wrapper class, itselfs exposes "visual" property. depending on use case, visual property either exposes contentpresenters' first , immediate visual child, or finds visual child name. i've cached instantiation of helper class, because otherwise converter create quite lot of these, , each time it'd query visual tree @ least once.
it tries keep property synchronized contentpresenter; while don't t hink there's direct way monitor if visual tree changes, went updating whenever contentpresenter's content property changes. (another way might update whenever layout changes, gets triggered lot in various cases, seemed overkill)
[valueconversion(typeof(contentpresenter), typeof(contentpresentervisualhelper))] public class contentpresentertovisualhelperconverter : ivalueconverter { /// <param name="parameter"> /// 1. can null/empty, in case first visual child of contentpresenter returned helper /// 2. can string, in case contentpresenter's child given name returned /// </param> public object convert(object value, type targettype, object parameter, system.globalization.cultureinfo culture) { if (value == null) return null; contentpresenter cp = value contentpresenter; if (cp == null) throw new invalidoperationexception(string.format("value must of type contentpresenter, {0}", value.gettype().fullname)); return contentpresentervisualhelper.getinstance(cp, parameter string); } public object convertback(object value, type targettype, object parameter, system.globalization.cultureinfo culture) { throw new notimplementedexception(); } } /// <summary> /// exposes either /// a) contentpresenter's immediate visual child, or /// b) of contentpresenter's visual children name /// in contentpresentervisualhelper's "visual" property. implements inotifypropertychanged notify when visual replaced. /// </summary> public class contentpresentervisualhelper : bindablebase, idisposable { private static object cachelock = new object(); private static memorycache cache = new memorycache("contentpresentervisualhelpercache"); protected readonly contentpresenter contentpresenter; protected readonly compositedisposable subscriptions = new compositedisposable(); protected readonly string childname; private frameworkelement _visual; public frameworkelement visual { { return _visual; } private set { this.setproperty(ref _visual, value); } } /// <summary> /// creates unique cache key combination of contentpresenter + childname /// </summary> private static string createkey(contentpresenter contentpresenter, string childname) { var hash = 17; hash = hash * 23 + contentpresenter.gethashcode(); if (childname != null) hash = hash * 23 + childname.gethashcode(); var result = hash.tostring(); return result; } /// <summary> /// creates instance of contentpresentervisualhelper given contentpresenter , childname, if necessary. /// or returns existing 1 cache, if available. /// </summary> public static contentpresentervisualhelper getinstance(contentpresenter contentpresenter, string childname) { string key = createkey(contentpresenter, childname); var cachedobj = cache.get(key) contentpresentervisualhelper; if (cachedobj != null) return cachedobj; lock (cachelock) { cachedobj = cache.get(key) contentpresentervisualhelper; if (cachedobj != null) return cachedobj; var obj = new contentpresentervisualhelper(contentpresenter, childname); var cacheitem = new cacheitem(key, obj); var expiration = datetimeoffset.now + timespan.fromseconds(60); var policy = new cacheitempolicy { absoluteexpiration = expiration }; cache.set(cacheitem, policy); return obj; } } private contentpresentervisualhelper(contentpresenter contentpresenter, string childname) { this.contentpresenter = contentpresenter; this.childname = childname; .contentpresenter .observedp(x => x.content) // extension method creates iobservable<object>, pushing values , whenever "contentproperty"-dependency property changes .distinctuntilchanged() .subscribe(x => contentpresenter_layoutupdated()) .makedisposable(this.subscriptions); // extension method adds idisposable this.subscriptions /* * alternative way? not * observable.fromeventpattern(contentpresenter, "layoutupdated") .throttle(timespan.frommilliseconds(50)) .subscribe(x => contentpresenter_layoutupdated()) .makedisposable(this.subscriptions);*/ } public void dispose() { this.subscriptions.dispose(); } void contentpresenter_layoutupdated() { trace.writeline(string.format("{0:hh.mm.ss:ffff} content presenter updated: {1}", datetime.now, contentpresenter.content)); if(!string.isnullorwhitespace(this.childname)) { // visual child name var child = this.contentpresenter.findchild<frameworkelement>(this.childname); // extension method, readily available on stackoverflow etc. this.visual = child; } else { // don't child name, rather // first - , - immediate visual child of contentpresenter var n = visualtreehelper.getchildrencount(this.contentpresenter); if (n == 0) { this.visual = null; return; } if (n > 1) throw new invalidoperationexception("contentpresenter had more 1 visual children"); var child = visualtreehelper.getchild(this.contentpresenter, 0); var _child = (frameworkelement)child; this.visual = _child; } } }
Comments
Post a Comment