Data Binding Dynamic Data

Data binding dynamic data

Welcome to the wonderful world of System.ComponentModel. This dark corner of .NET is very powerful, but very complex.

A word of caution; unless you have a lot of time for this - you may do well to simply serialize it in whatever mechanism you are happy with, but rehydrate it back into a DataTable at each end... what follows is not for the faint-hearted ;-p

Firstly - data binding (for tables) works against lists (IList/IListSource) - so List<T> should be fine (edited: I misread something). But it isn't going to understand that your dictionary is actually columns...

To get a type to pretend to have columns you need to use custom PropertyDescriptor implementations. There are several ways to do this, depending on whether the column definitions are always the same (but determined at runtime, i.e. perhaps from config), or whether it changes per usage (like how each DataTable instance can have different columns).

For "per instance" customisation, you need to look at ITypedList - this beast (implemented in addition to IList) has the fun task of presenting properties for tabular data... but it isn't alone:

For "per type" customisation, you can look at TypeDescriptionProvider - this can suggest dynamic properties for a class...

...or you can implement ICustomTypeDescriptor - but this is only used (for lists) in very occasional circumstances (an object indexer (public object this[int index] {get;}") and at least one row in the list at the point of binding). (this interface is much more useful when binding discrete objects - i.e. not lists).

Implementing ITypedList, and providing a PropertyDescriptor model is hard work... hence it is only done very occasionally. I'm fairly familiar with it, but I wouldn't do it just for laughs...


Here's a very, very simplified implementation (all columns are strings; no notifications (via descriptor), no validation (IDataErrorInfo), no conversions (TypeConverter), no additional list support (IBindingList/IBindingListView), no abstraction (IListSource), no other other metadata/attributes, etc):

using System.ComponentModel;
using System.Collections.Generic;
using System;
using System.Windows.Forms;

static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
PropertyBagList list = new PropertyBagList();
list.Columns.Add("Foo");
list.Columns.Add("Bar");
list.Add("abc", "def");
list.Add("ghi", "jkl");
list.Add("mno", "pqr");

Application.Run(new Form {
Controls = {
new DataGridView {
Dock = DockStyle.Fill,
DataSource = list
}
}
});
}
}
class PropertyBagList : List<PropertyBag>, ITypedList
{
public PropertyBag Add(params string[] args)
{
if (args == null) throw new ArgumentNullException("args");
if (args.Length != Columns.Count) throw new ArgumentException("args");
PropertyBag bag = new PropertyBag();
for (int i = 0; i < args.Length; i++)
{
bag[Columns[i]] = args[i];
}
Add(bag);
return bag;
}
public PropertyBagList() { Columns = new List<string>(); }
public List<string> Columns { get; private set; }

PropertyDescriptorCollection ITypedList.GetItemProperties(PropertyDescriptor[] listAccessors)
{
if(listAccessors == null || listAccessors.Length == 0)
{
PropertyDescriptor[] props = new PropertyDescriptor[Columns.Count];
for(int i = 0 ; i < props.Length ; i++)
{
props[i] = new PropertyBagPropertyDescriptor(Columns[i]);
}
return new PropertyDescriptorCollection(props, true);
}
throw new NotImplementedException("Relations not implemented");
}

string ITypedList.GetListName(PropertyDescriptor[] listAccessors)
{
return "Foo";
}
}
class PropertyBagPropertyDescriptor : PropertyDescriptor
{
public PropertyBagPropertyDescriptor(string name) : base(name, null) { }
public override object GetValue(object component)
{
return ((PropertyBag)component)[Name];
}
public override void SetValue(object component, object value)
{
((PropertyBag)component)[Name] = (string)value;
}
public override void ResetValue(object component)
{
((PropertyBag)component)[Name] = null;
}
public override bool CanResetValue(object component)
{
return true;
}
public override bool ShouldSerializeValue(object component)
{
return ((PropertyBag)component)[Name] != null;
}
public override Type PropertyType
{
get { return typeof(string); }
}
public override bool IsReadOnly
{
get { return false; }
}
public override Type ComponentType
{
get { return typeof(PropertyBag); }
}
}
class PropertyBag
{
private readonly Dictionary<string, string> values
= new Dictionary<string, string>();
public string this[string key]
{
get
{
string value;
values.TryGetValue(key, out value);
return value;
}
set
{
if (value == null) values.Remove(key);
else values[key] = value;
}
}
}

angular 11 form dynamic data binding

I think your approach can be improved for a better user experience, consider this

  1. Same as your approach, a user can add more fields but do not bind this value to the form
  2. Include a Delete button

This is how your TS File will look like

  projectForm = this.formBuilder.group({
jobs: this.formBuilder.array([])
});

get jobs() {
return this.projectForm.get("jobs") as FormArray;
}
deleteJobField(i) {
this.jobs.controls.splice(i, 1);
this.jobs.updateValueAndValidity()
}
addJobField($event) {
$event.preventDefault();
console.log( Array(this.additionalJobFields))
Array(this.additionalJobFields).fill(1).forEach(() => {
this.jobs.push(
this.formBuilder.group({
name: "",
salary: 0
})
);
});
this.additionalJobFields = null;
}
additionalJobFields = null;

And in your html

<form [formGroup]="projectForm" (ngSubmit)="onSubmit()">
<div>
<ng-container formArrayName="jobs">
<div *ngFor="let job of jobs.controls; let j = index" [formGroupName]="j">>
<ng-container style="width: 2px;">
<input type="text" placeholder="job name" formControlName="name">
</ng-container>
<ng-container style="width: 1px">
<input type="number" placeholder="job salary e.g. 3.4" formControlName="salary">

<button (click)='deleteJobField(j)' type="button">DEL</button>
</ng-container>
</div>
</ng-container>

<button>Show Values</button>
</div>
Add More Jobs:<input type="number" (keydown.enter)="addJobField($event)" [(ngModel)]="additionalJobFields" [ngModelOptions]="{standalone: true}"><br>
</form>
<pre>{{ projectForm.value | json }}</pre>

The basic idea is that when a user enters a value in the input field for adding more jobs and presses enter, we update the number of fields in the form array.

We also add a delete button against each input category and update form array when this is clicked

See this demo

Data-binding with dynamically added elements

Since you prefer to create the layout dynamically yourself, I'd propose the following approach. It's completely theoretical at the moment, but I don't see a reason, why this should not work for you.

Considering you set the Map with a custom attribute for binding.

<LinearLayout ...
app:inflateData="@{dataMap}" />

You then have to create a @BindingAdapter to handle what the adapter would do for you.

@BindingAdapter({"inflateData"})
public static void inflateData(LinearLayout layout, Map<String, Double> data) {
LayoutInflater inflater = LayoutInflater.from(layout.getContext());
for (Entry<String, Double> entry : data.entrySet()) {
MyItem myItem = inflater.inflate(R.layout.my_item, layout, true);
myItem.setKey(entry.getKey);
myItem.setValue(entry.getValue);
}
}

Here you inflate the layout for the item and add it to the parent layout. You could generalize it even more by adding more attributes to the layout and binding adapter.

data binding with dynamic set content view

Get rid of

updateLayout(R.layout.content_first);

and change

ContentFirstBinding mBinding = DataBindingUtil.setContentView(this, R.layout.content_first);

with

ContentFirstBinding mBinding = DataBindingUtil.inflate(getLayoutInflater(), R.layout.content_first, (FrameLayout) findViewById(R.id.content_frame), true)

the last line will take care of inflate the layout for you, and add its content to (FrameLayout) findViewById(R.id.content_frame)

How to use two-way data binding in Android for dynamically created fields

Data Binding doesn't really work with code-generated layouts. That said, you can use data binding to do this with a Binding Adapter.

This article tells you how to use data binding with lists:

https://medium.com/google-developers/android-data-binding-list-tricks-ef3d5630555e#.v2deebpgv

If you want to use an array instead of a list, it is a small adjustment. If you want to use a map, you'll have to pass the map as a parameter to the bound layout. The article assumes a single variable, but you can pass in multiple in your BindingAdapter. For a simple Binding Adapter:

@BindingAdapter({"entries", "layout", "extra"})
public static <T, V> void setEntries(ViewGroup viewGroup,
T[] entries, int layoutId,
Object extra) {
viewGroup.removeAllViews();
if (entries != null) {
LayoutInflater inflater = (LayoutInflater)
viewGroup.getContext()
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
for (int i = 0; i < entries.length; i++) {
T entry = entries[i];
ViewDataBinding binding = DataBindingUtil
.inflate(inflater, layoutId, viewGroup, true);
binding.setVariable(BR.data, entry);
binding.setVariable(BR.extra, extra);
}
}
}

And you would bind the entries like this: (assume entry.xml)

<layout>
<data>
<variable name="data" type="String"/>
<variable name="extra" type="java.util.Map<String, String>"/>
</data>
<EditText xmlns:android="..."
android:text="@={extra[data]}"
.../>
</layout>

And in your containing layout, you'd use:

<layout>
<data>
<variable name="person" type="com.example.Person"/>
<variable name="attributes" type="String[]"/>
</data>
<FrameLayout xmlns:android="..." xmlns:app="...">
<!-- ... -->
<LinearLayout ...
app:layout="@{@layout/entry}"
app:entries="@{attributes}"
app:extra="@{person.attributes}"/>
</FrameLayout>
</layout>

Angular 13: Dynamic Html with data binding

Assuming:
You will add html content after data is loaded.

Then just replace this:

this.htmlContent = `<p class="email">Email: {{data.email}}</p>
<br><p class="mobile">Mobile: {{data.mobile}}</p>`;

with:

this.htmlContent = `<p class="email">Email: ${this.data.email}</p>
<br><p class="mobile">Mobile: ${this.data.mobile}</p>`;

If you are adding content using this.htmlContent then you don't need to use data-binding because you can use the feature of javascript template literals.

Dynamic object two way data binding

When setting up data binding to a property, framework invokes AddValueChanged method of the PropertyDescriptor of that property. To provide two-way data binding, your property descriptor should override that method and subscribe for PropertyChanged event of the component and call OnValueChanged method of the property descriptor:

void PropertyChanged(object sender, EventArgs e)
{
OnValueChanged(sender, e);
}
public override void AddValueChanged(object component, EventHandler handler)
{
base.AddValueChanged(component, handler);
((INotifyPropertyChanged)component).PropertyChanged += PropertyChanged;
}
public override void RemoveValueChanged(object component, EventHandler handler)
{
base.RemoveValueChanged(component, handler);
((INotifyPropertyChanged)component).PropertyChanged -= PropertyChanged;
}

Example

You can find a working implementation in the following repository:

  • r-aghaei/DynamicObjectTwoWayDataBinding


Related Topics



Leave a reply



Submit