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
- Same as your approach, a user can add more fields but do not bind this value to the form
- 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
Xml Serialize Generic List of Serializable Objects
Parallel.Foreach Slower Than Foreach
Datagridview Bound to a Dictionary
How to Pass Parameters to Another Process in C#
Embedding One Dll Inside Another as an Embedded Resource and Then Calling It from My Code
C# Elegant Way to Check If a Property's Property Is Null
Detect Webbrowser Complete Page Loading
Abstract Classes VS Interfaces
How to List All Processes Running in Windows
How to Find and Replace Text in a File
How to Quickly Check If Two Data Transfer Objects Have Equal Properties in C#
Does Parallel.Foreach Limit the Number of Active Threads
Exception from Hresult: 0X800A03Ec Error
How to Justify Text in a Label
C#:What If a Static Method Is Called from Multiple Threads