Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Two-way bind a "virtual" list of strings to a column

I have a list of Strings.

Well, conceptually. They are stored somewhere else, but I want provide an object which acts like a list (and provides any necessary events on top of that), with properties that I could bind to.

I want to establish a two-way binding over this data, to display it as a modifiable column in a DataGrid. I have the following problems with that:

  • I can't make a two-way binding because the binding needs a path (i.e. I can't have it look like {Binding} or {Binding Path=.} in the column, must be {Binding Path=someField"} to be made modifiable if I got this right, which sounds reasonable).
  • I don't exactly know how the proxy collection object should look like, in terms of interfaces (would IEnumerable + INotifyCollectionChanged sufficient?)

Is there any solution which doesn't involve creating one proxy object per every String in the collection? Could you suggest an efficient design?


To keep the discussion on the rails, let's assume I want to bind to something like this:

class Source {
    public String getRow(int n);
    public void setRow(int n, String s);
    public int getCount();
    public void addRow(int position, String s);
    public void removeRow(int position);
}

That's not exactly my case, but when I know how to bind to this, I think I'll be able to handle any situation like this.

I'm OK with having to provide an adapter object on top of that Source, with any necessary interfaces and events, but I don't want to have one adapter object per row of data.

like image 436
Kos Avatar asked Mar 14 '12 21:03

Kos


1 Answers

While making an adapter for the Source is relatively clear, then, unfortunatelly, the core of the second problem ('not wrapping every string in a miniobject') is a clash built into the .Net and WPF..

The first thing is that the WPF does provide you with many ways of registering 'on data modified' callbacks, but provides no way of registering callbacks that would provide a value. I mean, the "set" phase is only extendable, not interceptable, and the "get" - nothing at all. WPF will simply keep and return whatever data it has once cached.

The second thing is that in .Net the string is ... immutable.

Now, if ever you provide a string directly as a pathless binding or as a datacontext to any control, you are screwed in a dead end. The problem is, that WPF actually passes only the actual value of the binding, without the information of "where it came from". The underlying control will be simply given the string instance, and will have no sane way of modifying it as the string cannot change itself. You will not be even notified about such attempt, just like with read-only properties. What's more - if you ever manage to intercept such a modification attempt, and if you produce a proper new string, the WPF will never ask you again for the new value. To update the UI, you'd have to mannually, literally, force the WPF to re-ask you by for example changing the original binding so it points elsewhere (to the new value) or set the datacontext (to the new instance). It is doable with some VisualTree scanning, as every 'changed' callback gives you the DependencyObjects (Controls!), so yo ucan scan upwards/downwards and tamper with their properties.. Remember that option - I'll refer to this in a minute.

So, everything boils down to the fact that to get a normal 2-way binding you do not have to have a Path, you "just" have to have a mutable underlying data object. If you have immutable one - then you have to use a binding to a mutable property that holds the immutable value..

Having said that, you simply have to wrap the strings some how if you want to modify them.

The other question is, how to do that. There's a plenty of ways to do it. Of course, you can simply wrap them like Joe and Davio suggested (note to Joe: INotify would be needed there also), or you can try to do some XAML tricks with attached properties and/or behaviours and/or converters to do that for you. This is completely doable, see for example my other post - I've shown there how to "inject a virtual property" that pulled the data completely from elsewhere (one binding+converter performed the wrapping on the fly, second binding extracted the values from the attached-wrapper). This way you could create a "Contents" property on the string, and that property could simply return the string itself, and it'd be completely 2-way bindable with no exceptions.

But.. it would NOT work 2-way-ish.

Somewhere at the root of your binding/behaviour/conveter chain, there will be an immutable string. Once your smart autowrapping binding chain fires with 'on modified' callback you will be notified with pair of old/new values. You will be able to remap the values to new and old strings. If you implemented everything perfectly, the WPF will simply use the new value. If you tripped somewhere, then you will have to push the new value artificially back to the UI (see the options I'd asked you to remember). So, it's ok. No wrapper, old value was visible, it was changeable, you've got new value, the UI displays new value. How about storage?

Somewhere in the meantime you've been given a old/new value pair. If you analyze them, you'll get old/new strings. But how do you update the old immutable string? Can't do. Even if autowrapping worked, even if UI worked, even if editing seemed to work, you are now standing with the real task: you onmodified callback was invoked and you have to actually update that immutable string piece.

First, you need your Source. Is it static? Phew. What a luck! So surely it is instanced. In the on-modified callback we got only a old+new string.. how to get the Source instance? Options:

  • scan the VisualTree and search for it in the datacontexts and use whatever was found..
  • add some more attached properties and binding to bind a virtual "Source" property to every string and read that property from the new value

Well doable, but smells, but no other options.

Wait, there's more: not only the old/new value and an instance of Source are needed! You also need the ROW INDEX. D'oh! how to get that from the bound data? Again, options:

  • scan the VisualTree and search for it (blaargh)...
  • add some more attached properties and bindings to bind a virtual "RowIndex" property to every (blaaergh)...

At this point of time, while I see that all of this seems implementable and actually might be working properly, I really think that wrapping each string in a small

public class LocalItem // + INotifyPropertyChanged
{
    public int Index { get; }
    public Source Source { get; }

    public string Content
    {
        get { Source...}
        set { Source... }
    }
}

will simply be more readable, elegant and .. SHORTER to implement. And less error-prone, as more details will be explicit instead of some WPF's binding+attached magic..

like image 107
quetzalcoatl Avatar answered Oct 25 '22 03:10

quetzalcoatl