I am creating WPF elements dynamically in code behind, and for each of the rows in the Grid I'm building it consists of a CheckBox and a Dynamic number of TextBoxes. The interaction that is needed is the following:
If all TextBoxes in a row have a value of 0, set the CheckBox
IsChecked property to true and Disable it.
If one of the TextBoxes is then changed from 0, enable the
CheckBox and set IsChecked to false.
If the user clicks on the CheckBox, set all associated TextBoxes
to 0 and Disable the CheckBox
I was able to accomplish the first part of the last one using this code:
Binding setScoreToZeroIfIsNormalChecked = new Binding("IsChecked");
setScoreToZeroIfIsNormalChecked.Source = this.NormalCheckBoxControl;
setScoreToZeroIfIsNormalChecked.Converter = m_NormalCheckBoxJointScoresConverter;
tempJointScoreControl.JointScoreContainer.SetBinding(ContainerBase.SingleAnswerProperty, setScoreToZeroIfIsNormalChecked);
and the converter:
public object Convert(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value is bool && targetType == typeof(Answer))
{
if ((bool)value)
{
Answer answer = new Answer();
answer.Value = "0";
answer.DisplayValue = "0";
return answer;
}
else
return null;
}
else
{
return null;
}
}
However, in attempting to create another converter to accomplish other functionality, I was running into issues of converters stepping on one another since all functionality is based around the CheckBox.IsChecked property.
Is there anyway to accomplish all of the above using one or two multibinding converters? I'd really like to avoid having to create a whole bunch of events and maintaining them in order to do this.
It's relatively easy. Everything should resolve around CheckBox IsChecked property.
For a simple reason, it's a two-way property. So either you can modify it, or CheckBox can modify it.
So what you do, you use MultiBinding, as such:
MultiBinding multiBinding = new MultiBinding();
multiBinding.Converter = multiBindingConverter;
multiBinding.Bindings.Add(new Binding("Text") { Source = txtbox1});
multiBinding.Bindings.Add(new Binding("Text") { Source = txtbox2});
multiBinding.NotifyOnSourceUpdated = true;//this is important.
checkBox.SetBinding(CheckBox.IsCheckedProperty, multiBinding);
And in your multiBindingConverter, you will have object[] value as first parameter, which you need to convert into IList and iterate over it && do your calculations, if you should either return true/false.(IsChecked=true or false)
Now bind CheckBox IsEnabled to CheckBox IsChecked property, and use BooleanInverterConverter. (If CheckBox is checked, it should be disabled, and vice versa)
The last step is to make TextBoxes listen to actual IsChecked property of CheckBox.
If it is TRUE, they all should show value of 0, otherwise they can show what they want.
So, make a new MultiBinding.
MultiBinding multiBinding = new MultiBinding();
multiBinding.Converter = textboxMultiBindingConverter;
multiBinding.Bindings.Add(new Binding("IsChecked") { Source = checkbox1});
multiBinding.Bindings.Add(new Binding("Text") { Source = textbox1});
multiBinding.NotifyOnSourceUpdated = true;//this is important.
textbox1.SetBinding(TextBox.Text, multiBinding);
the idea in textboxMultiBindingConverter is to either return Text(value[1]) if value[0]==FALSE or "0" if value[0]==TRUE.
This problem can be solved very easily if you would use MVVM.
You would have a ViewModel that represents a row in the grid. It would have a property per textbox and one for the checkbox.
Additionally you would have a ViewModel for the View containing the Grid and this ViewModel would expose a collection of row ViewModels.
The ViewModel for your row:
public class AnswersViewModel : ViewModelBase // From MvvmLight
{
public bool IsAnswered
{
get { return _isAnswered; }
set
{
if(value == _isAnswered)
return;
_isAnswered = value;
if(_isAnswered)
{
Answer1 = "0";
Answer2 = "0";
}
RaisePropertyChanged("IsAnswered");
}
}
public string Answer1
{
get { return _answer1; }
set
{
if(value == _answer1)
return;
_answer1 = value;
RaisePropertyChanged("Answer1");
if(_answer1 == "0" && _answer2 == "0")
{
_isAnswered = true;
RaisePropertyChanged("IsAnswered");
}
}
}
// The implementation of Answer2 is similar to Answer1
}
The ViewModel for the View:
public class FooViewModel : ViewModelBase
{
public ObservableCollection<AnswersViewModel> Answers
{
get { return _answers; }
}
}
Your View would contain the Grid with ItemsSource="{Binding Answers}" and a ControlTemplate for the items which binds to the properties of AnswersViewModel.
Disabling the CheckBox I would handle via a Trigger in a Style.
Related
I'm making a small application, it's a form that reads from a data source, I want to use it for editing and adding new records.
so the default binding mode for the textboxes in the form is TwoWay mode, so the user can edit an existing record, but I want to add a Checkbox that when checked, it marks the data in the textboxes as new, and then adding them to the data source, so I need to change the binding mode to OneWay,
to my knowledge, to do this in code I need to create a new Binding object, that I will have to set properties like Source that doesn't change:
Binding myBinding = new Binding();
myBinding.Source = ViewModel;
myBinding.Path = new PropertyPath("SomeString");
myBinding.Mode = BindingMode.TwoWay;
myBinding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
So is there a way to change only the Binding mode in code?
EDIT
some further explanation of the application:
In the form there's a combobox that is bound to a List<Book>, there are 3 TextBoxs, their Text properties bound to the DataContext object of their container which itself set to the SelectedItem of the Combobox.
When I added the ReadOnly property as described in the answer, when I check the checkbox I can't change the text in the textboxes.
..
Thanks!
Don't change Mode of binding. Just correct your view-model logic.
public class ViewModel : INotifyPropertyChanged
{
private string _text;
private bool _readOnly;
public string Text
{
get { return _text; }
set
{
if (ReadOnly || value == _text)
return;
_text = value;
OnPropertyChanged(nameof(Text));
}
}
public string ReadOnly
{
get { return _readOnly; }
set
{
if (value == _readOnly)
return;
_readOnly = value;
OnPropertyChanged(nameof(ReadOnly));
}
}
}
In XAML bind IsChecked property of your CheckBox to ReadOnly property.
The answer to this is "Yes, but". The Mode property of Binding has a setter. So it appears that you can set the mode of an existing binding like so...
BindingExpression be = textBox.GetBindingExpression(TextBox.TextProperty);
Binding b = be?.ParentBinding as Binding;
if (b != null)
{
b.Mode = BindingMode.OneWay;
}
However if you do this you will get in every case the exception...
System.InvalidOperationException occurred
HResult=0x80131509
Message=Binding cannot be changed after it has been used.
So the only way to accomplish what you want is to create a new binding based on the old binding while changing the mode. Then replace the old binding.
BindingExpression be = textBox.GetBindingExpression(TextBox.TextProperty);
Binding b = be?.ParentBinding as Binding;
if (b != null)
{
Binding b2 = new Binding();
b2.Path = b.Path;
b2.Mode = BindingMode.OneWay;
textBox.SetBinding(TextBox.TextProperty, b2);
}
This is less than ideal because for completeness you would need to copy the converter, the converter parameter and so on, but this is the best you can do.
I am working on a WPF application and i have a textbox bound (bidirectionally) to a property in my view model.
I am trying to prevent a user from typing more than 100 characters into this textbox (this is the max the database will store) so i have written this.
public abstract class AppBaseViewModel : ViewModelBase
{
private String _text;
public String Text
{
get { return _text; }
set
{
_text = CheckTextLength(value, _text);
OnPropertyChanged("Text");
}
}
private string CheckTextLength(string value, string text)
{
if (value.Length < 100)
{
return value;
}
else
{
return text;
}
}
}
All this code seems to do is save the first 100 characters to the field but it still allows the user to carry on typing past 100 characters... i would guess it is because the field value isn't being passed back to the textbox.
I don't understand why this doesn't work as i did something similar using MVVM Light's RaisePropertyChange() in a different application.
It is worth noting that i am unable to access the designer for the textbox so cannot set the .Net textbox property for max length.
Edit: Just for clarification i cannot view or edit the xaml as some are suggesting as i do not have access to the XAML file (i know, it's stupid). All the bindings we use are two way by default
Have you tried with TextBox.MaxLength ?
<TextBox MaxLength="100"/>
Gets or sets the maximum number of characters that can be manually entered into the text box.
If no access to the XAML, eventually get access to the XAML instead of parsing and verifying lengths of arrays and use substrings here and there. At least that's what i would do for this simple issue or talk to the designer to add that small piece of code.
Update 1
public static T GetChildOfType<T>(DependencyObject depObj) where T : DependencyObject
{
if (depObj == null) return null;
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
var child = VisualTreeHelper.GetChild(depObj, i);
var result = (child as T) ?? GetChildOfType<T>(child);
if (result != null) return result;
}
return null;
}
Go and get that child and set its MaxLength. This is just a slight modification on the View so it will not affect the MVVM pattern.
OK. I'm not at all sure that I'm proud of this, but am presenting it as an alternative.
You can change the UpdateSourceTrigger of the TextBox's Text property by applying a universal Style to all of the TextBoxes. This is only going to be practical in pretty weird arrangements, but the question is a little unusual in itself.
XAML codebehind:
//I'm using MVVM Light here - you need to be able to find an instance
//of your AppBaseViewModel somehow.
private ViewModelLocator _locator;
//View codebehind constructor, may need to change names as appropriate
public AppBaseView()
{
InitializeComponent();
//MVVM Light again
_locator = new ViewModelLocator();
//Create the binding
Binding binding = new Binding();
//Source = The instance of your ViewModel
binding.Source = _locator.AppBaseViewModel ;
binding.Path = new PropertyPath("Text");
binding.Mode = BindingMode.TwoWay;
binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
//Create a Style with no Key - this will apply to *all* TextBoxes
//without their own explicit Style set.
Style style = new Style(typeof(TextBox));
style.Setters.Add(new Setter(TextBox.TextProperty, binding));
//Add the Style to the XAML's Resources:
Resources.Add(typeof(TextBox), style);
}
The view won't listen to the PropertyChanged notification if it's currently trying to change the property itself.
The only thing that comes to mind is launching an extra delayed PropertyChanged notification when you detect the constraint is not met...
private string CheckTextLength(string value, string text)
{
if (value.Length < 100)
{
return value;
}
else
{
MyDispatcher.BeginInvoke(new Action(() =>
OnPropertyChanged("Text")),
DispatcherPriority.Loaded);
return text;
}
}
Can't try the code, so sorry if it doesn't build righ away. MyDispatcher could be your Application.Current.Dispatcher, for instance.
The xaml view /the binding is only updated when the textbox has lost focus. if the text entered is <100 then the value is set otherwise _text is set. this means that initially _text has no value so null will be set upon the if statement being false. i also suggest yo use RaisePropertyChanged(); and when used within the property itself no parameter is needed.
I have a ViewModel which contains an ObservableCollection<CustomKeyGroup<CustomItem>> property bound to a control in a View and the problem is that I want to sort this collection by a property in CustomKeyGroup<T>, without setting the ObservableCollection<...> object property (i.e. sort the collection inline):
public class MainViewModel : ViewModelBase {
... // data service etc code
private ObservableCollection<CustomKeyGroup<CustomItem>> _items = new ObservableCollection<CustomKeyGroup<CustomItem>>();
public ObservableCollection<CustomKeyGroup<CustomItem>> Items
{
get
{
return _items;
}
set
{
_items = value;
RaisePropertyChanged("Items");
}
}
public void Sort(string _orderBy = null, bool _descending = true) {
if (string.IsNullOrEmpty(_orderBy) || this.Items.Count == 0) {
return;
}
var test = this.Items.ToList();
// bubble sort
try {
for (int i = test.Count - 1; i >= 0; i--) {
for (int j = 1; j <= i; j++) {
CustomKeyGroup<CustomItem> o1 = test[j - 1];
CustomKeyGroup<CustomItem> o2 = test[j];
bool move = false;
var order = typeof(CustomKeyGroup<CustomItem>).GetProperty(orderBy);
var t = order.GetValue(o1);
var t2 = order.GetValue(o2);
// sort comparisons depending on property
if (_descending) { // ascending
if (t.GetType() == typeof(int)) { // descending and int property
if ((int)t < (int)t2) {
move = true;
}
} else { // descending and string property
if (t.ToString().CompareTo(t2.ToString()) > 0) {
move = true;
}
}
} else { // ascending
if (t.GetType() == typeof(int)) { // ascending and int property
if ((int)t > (int)t2) {
move = true;
}
} else { // ascending and string property
if (t.ToString().CompareTo(t2.ToString()) < 0) {
move = true;
}
}
}
// swap elements
if (move) {
//this.Items.Move(j - 1, j); // "inline"
test[j] = o1;
test[j - 1] = o2;
}
}
}
// set property to raise property changed event
this.Items = new ObservableCollection<CustomKeyGroup<CustomItem>>(test);
} catch (Exception) {
Debug.WriteLine("Sorting error");
}
//RaisePropertyChanged("Items"); // "inline sort" raise property changed to update Data binding
Debug.WriteLine("Sorted complete");
}
... // get data from service, etc.
From the code above, the attempted inline sorts are commented out (as they do not update the control that databinds to it), and the manual setting of Items are left in (works, but if you scroll down the control and sort, it will take you back to the top - undesirable!).
Anyone have any idea how I can update the view/control using an inline sort option? I've also tried manually raising the RaisePropertyChanged event (specified in ObservableObject using the MVVMLight Toolkit) to no avail.
Note: Setting a breakpoint at the end of the try-catch reveals that the ObservableCollection<...> is indeed sorted, but the changes just do not reflect in the View! Even weirder is that the control (LongListSelector) has a JumpList bound to another property of CustomKeyGroup<T> and it successfully updates instantly!! If I tap on any of these items in the JumpList, the View correctly updates itself, revealing the sorted items... I then thought of setting the DataContext of the View after sorting, but that also does not solve the issue.
Thanks.
Adding my own answer here.
So following the comments from the original post, #piofusco points out that a View does not update when an ObservableCollection has only been sorted. Even manually changing the collection (hence, raising NotifyPropertyChanged or NotifyCollectionChanged) does not update it.
Searching around a little more, I decided I could make use of CollectionViewSource, which would do my sorting for me - without changing the collection itself (hence allowing the control to retain its current scroll position). To get it working, basically, add a new property to the ViewModel of type CollectionViewSource, add a SortDescription, set its Source and bind directly to that property (instead of the original ObservableCollection:
In ViewModel:
private CollectionViewSource _sortedCollection = new CollectionViewSource();
public CollectionViewSource SortedCollection {
get {
_sortedCollection.Source = this.Items; // Set source to our original ObservableCollection
return _sortedCollection;
}
set {
if (value != _sortedCollection) {
_sortedCollection = value;
RaiseNotifyPropertyChanged("SortedCollection"); // MVVMLight ObservableObject
}
}
}
View XAML (note the binding to Property.View):
<ListBox ItemsSource="{Binding SortedCollection.View}" ... />
And in your View code-behind, if you have a Sort button:
ViewModel _vm = this.DataContext as ViewModel;
viewModel.SortedCollection.SortDescriptions.Clear(); // Clear all
viewModel.SortedCollection.SortDescriptions.Add(new SortDescription("PropertyName", ListSortDirection.Descending)); // Sort descending by "PropertyName"
And boom! Your sorted collection should update instantly in the View! Even better is that it retains our ObservableCollection functionality in that any updates to objects in the ObservableCollection will raise the NotifyPropertyChanged or NotifyCollectionChanged handlers, thereby updating the View (allowing for both sorting and updating of objects while retaining current scroll positions)!
Note: For those out there using a LongListSelector control, I wasn't able to get it to work, and with a little more internet-digging with I came across this post, which, discusses why LLS cannot bind to a CollectionViewSource.View without some modifications. So I ended up using a ListBox control instead. You can read about some of the differences here. For my task though, the ListBox will suffice.
Here's a concrete example of what I am attempting to do with the Telerik GridView control. Let's say I have an application that will read a delimited file (say CSV) which will have n columns. The value n can vary from file-to-file but is constant within a file. The application starts with an empty grid and adds columns as required to match the input file. The grid displays all data as strings in all cells. This works with either binding to a BindingList or putting the record (objects) into the GridView.Items list.
Now, what I want to do is put a single row at the top of the grid (a row that will not scroll) that contains comboboxes. That is, at the top of each column, the first row contains a combobox. On the first pass, the combobox will only be a drop list, but on the next pass I will add another row with a set of comboboxes that will be editable. For now, let's only consider drop lists.
The specific problem that I have is that I do not see how to set a specific type of control for a particular cell. Telerik provides a GridViewComboBoxColumn class that will define the behavior for an entire column but that's not what I need.
Because of the variable number of columns, I think that the code-behind would be the place to work this magic. I may have to do something in the xaml but, since I've only been in WPF for a few months, nothing is jumping out at me.
I've done something like this with the DataGridView and XtraGrid, but this one has me stumped. Pointers would be much appreciated.
In response to Jonathan D's answer, I have taken the provided code and modified it to recognize when a cell on the 0th row is being edited. When this is the case, a drop list is presented when the user initiates an edit operation.
using Telerik.Windows.Controls;
using Telerik.Windows.Controls.GridView;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace LLamasoft.DataGuru.Plugin.Internal.ConfigurationUI
{
public class RadGridViewComboboxHeaderColumn : GridViewBoundColumnBase
{
public static readonly DependencyProperty SelectedStringProperty =
DependencyProperty.Register("SelectedString", typeof(string), typeof(RadGridViewComboboxHeaderColumn), new PropertyMetadata(null));
public string SelectedString
{
get { return (string) GetValue(SelectedStringProperty); }
set { SetValue(SelectedStringProperty, value); }
}
public override FrameworkElement CreateCellEditElement(GridViewCell cell, object dataItem)
{
// we need the row on which this cell lives
GridViewDataControl gridViewDataControl = cell.ParentRow.GridViewDataControl;
object currentEditItem = gridViewDataControl.Items.CurrentEditItem;
int index = gridViewDataControl.Items.IndexOf(currentEditItem);
FrameworkElement frameworkElement = null;
if (index == 0)
{
BindingTarget = ComboBox.SelectedValueProperty;
ComboBox comboBox = new ComboBox();
// seed some values,
// this list should be set right after construction if static, otherwise via callback if dynamic
comboBox.Items.Add(string.Empty);
comboBox.Items.Add("apples");
comboBox.Items.Add("oranges");
if (!comboBox.Items.Contains(cell.Value))
{
comboBox.Items.Add(cell.Value);
}
comboBox.SelectedValue = SelectedString;
frameworkElement = comboBox;
}
else
{
BindingTarget = TextBox.TextProperty;
TextBox textBox = new TextBox();
textBox.Text = SelectedString;
frameworkElement = textBox;
}
frameworkElement.SetBinding(BindingTarget, CreateValueBinding());
return frameworkElement;
}
public override object GetNewValueFromEditor(object editor)
{
// ensure that the control will return the correct value when queried for it
ComboBox comboBox = editor as ComboBox;
if (comboBox != null)
{
// bound to comboBox.SelectedValue which carries the correct value
}
TextBox textBox = editor as TextBox;
if (textBox != null)
{
// bound to textBox.Text which carries the correct value
}
return base.GetNewValueFromEditor(editor);
}
private Binding CreateValueBinding()
{
Binding valueBinding = new Binding();
valueBinding.Mode = BindingMode.TwoWay;
valueBinding.NotifyOnValidationError = true;
valueBinding.ValidatesOnExceptions = true;
valueBinding.UpdateSourceTrigger = UpdateSourceTrigger.Explicit;
valueBinding.Path = new PropertyPath(this.DataMemberBinding.Path.Path);
return valueBinding;
}
}
}
The good news is that this shows that any edit control can be used in any cell based on requirements.
The bad parts are: 1) a dummy record has to be inserted at the 0th list position and must be maintained there, 2) the data is being stored back into the field on the 0th record and may require a different data type than is on the equivalent fields on the other records, and 3) the combobox is only shown when the cell is in the edit mode.
The latter issue for me may not be an issue elsewhere. I want a visual cue that the user is expected to interact with the cells at the top of the columns. Using this method, there is no differentiating factor between the top row and the rest of the rows until the edit operation begins. My ideal solution would have the cells always show their comboboxes.
One other issue that I find difficult to believe that I am facing is the fact that I cannot easily pin/freeze topmost rows. I want this line to always remain at the top after scrolling. There is no _grid.Rows[0].IsPinned = true functionality.
Telerik has responded to my request for info and suggests that I use a template selector to determine how the cell is represented. (http://www.telerik.com/community/forums/wpf/gridview/need-just-first-row-in-grid-to-be-all-comboboxes.aspx#1820310). At this point, I turn my attention to testing that method.
You want to create your own column
using System.Windows;
using System.Windows.Data;
using Telerik.Windows.Controls;
using Telerik.Windows.Controls.GridView;
using System;
namespace Inspect
{
public class DateTimePickerColumn : GridViewBoundColumnBase
{
public TimeSpan TimeInterval
{
get
{
return (TimeSpan) GetValue(TimeIntervalProperty);
}
set
{
SetValue(TimeIntervalProperty, value);
}
}
public static readonly DependencyProperty TimeIntervalProperty =
DependencyProperty.Register("TimeInterval", typeof(TimeSpan), typeof(DateTimePickerColumn), new PropertyMetadata(TimeSpan.FromHours(1d)));
public override FrameworkElement CreateCellEditElement(GridViewCell cell, object dataItem)
{
this.BindingTarget = RadDateTimePicker.SelectedValueProperty;
RadDateTimePicker picker = new RadDateTimePicker();
picker.IsTooltipEnabled = false;
picker.TimeInterval = this.TimeInterval;
picker.SetBinding(this.BindingTarget, this.CreateValueBinding());
return picker;
}
public override object GetNewValueFromEditor(object editor)
{
RadDateTimePicker picker = editor as RadDateTimePicker;
if (picker != null)
{
picker.DateTimeText = picker.CurrentDateTimeText;
}
return base.GetNewValueFromEditor(editor);
}
private Binding CreateValueBinding()
{
Binding valueBinding = new Binding();
valueBinding.Mode = BindingMode.TwoWay;
valueBinding.NotifyOnValidationError = true;
valueBinding.ValidatesOnExceptions = true;
valueBinding.UpdateSourceTrigger = UpdateSourceTrigger.Explicit;
valueBinding.Path = new PropertyPath(this.DataMemberBinding.Path.Path);
return valueBinding;
}
}
}
That is how you create a custom column. If you modify the CreateCellEditElement Method it will let you create custom cells how you like. You should even be able to detect the row number
i wish to create a form at runtime that will read the columns for any datasource and create fields based on the columns and datatype just like a datagridviews insert line
Best regards,
Mark
What you are doing sounds a lot like how PropertyGrid already works, which is essentially:
foreach(PropertyDescriptor prop in TypeDescriptor.GetProperties(obj)) {
object val = prop.GetValue(obj);
string s = prop.Converter.ConvertToString(val);
Control cont = // TODO: create some control and set x/y
cont.Text = s;
this.Controls.Add(cont);
}
To avoid lots of work with alignment, using Dock to set the positions might help:
using(Form form = new Form())
using (PropertyGrid grid = new PropertyGrid())
{
form.Text = obj.ToString(); // why not...
grid.Dock = DockStyle.Fill;
form.Controls.Add(grid);
grid.SelectedObject = obj;
form.ShowDialog(this);
}
I wonder if it is easier to use PropertyGrid in simple circumstances, though. Or there are some 3rd-party versions that work similarly.
Ok so heres what i came up with!
public partial class Form2 : Form
{
private Boolean isBrowsable(PropertyInfo info)
{
return info.GetCustomAttributes(typeof(BrowsableAttribute), false).Length>-1;
}
public Form2()
{
InitializeComponent();
}
public Form2(Boolean showCheckBoxes)
{
InitializeComponent();
_showCheckBoxes = true;
}
private Boolean _showCheckBoxes;
private Object _reflection;
private TableLayoutPanel _table = new TableLayoutPanel{Dock=DockStyle.Fill, CellBorderStyle = TableLayoutPanelCellBorderStyle.Single};
public Object SelectedObject
{
get
{
return _reflection;
}
set
{
//clear all controls from the table
_table.Controls.Clear();
foreach (var property in _reflection.GetType().GetProperties())
{
if (isBrowsable(property))
{
if ((property.PropertyType == typeof(int)) || (property.PropertyType == typeof(string)))
{
var textField = new TextBox { Dock = DockStyle.Fill, AutoSize = true };
textField.DataBindings.Add("Text", _reflection, property.Name);
_table.Controls.Add(textField, 2, _table.RowCount += 1);
var propertyLabel = new Label
{
Text = property.Name,
Dock = DockStyle.Fill,
TextAlign = ContentAlignment.MiddleLeft
};
_table.Controls.Add(propertyLabel, 1, _table.RowCount);
if (_showCheckBoxes)
{
var checkBox = new CheckBox
{
AutoSize = true,
Name = property.Name,
Dock = DockStyle.Left,
CheckAlign = ContentAlignment.TopLeft
};
_table.Controls.Add(checkBox, 0, _table.RowCount);
}
}
}
}
//add one extra row to finish alignment
var panel = new Panel { AutoSize = true };
_table.Controls.Add(panel, 2, _table.RowCount += 1);
_table.Controls.Add(panel, 1, _table.RowCount);
if (_showCheckBoxes)
{
_table.Controls.Add(panel, 0, _table.RowCount);
}
Controls.Add(_table);
if (!Controls.Contains(_table))
Controls.Add(_table);
}
}
public Boolean Execute(Object reflection)
{
SelectedObject = reflection;
return ShowDialog() == DialogResult.OK;
}
}
thanks all!
I don't fully understand your question. Is it correct that you want to create a Windows form which provides input fields (textboxes, checkboxes, etc.) for all fields/properties of an object that you feed to the form as its DataSource?
You might have to use reflection for this (see the System.Reflection namespace). For example, to get a list of all properties:
using System.Reflection;
....
public object DataSource;
...
Debug.Assert( DataSource != null );
var properties = DataSource.GetType().GetProperties();
You would then instantiate one input control per property:
foreach ( var property in properties )
{
// extract some information about each property:
string propertyName = property.Name;
Type propertyType = property.PropertyType;
bool propertyReadOnly = !property.CanWrite;
// create input controls based on this information:
// ...
}
However, it might be fairly tricky to reliably map property types to the correct input control; for example, what are you going to do when you encounter a property with some unknown class as its type, or when a property is a collection of values? You might have to create a sub-form inside your form in some cases; in other cases, a listbox might be enough.
I've recently built a sample project that uses the Dynamic Data assemblies of ASP.NET to do just this for a WPF grid, but I'm sure you could adapt the concept to WinForms. Dynamic Data provides much richer metadata than just reflection or the database, but it does require an Entity Data Model, or a LINQ to SQL data model.
basically, all you need is a reference to System.Web.DymamicData, and maybe you can find something useful in my class:
public class DynamicDataGridBuilder<TContext, TEntity> where TEntity : EntityObject
{
readonly MetaModel model = new MetaModel();
public DynamicDataGridBuilder()
{
model.RegisterContext(typeof(TContext), new ContextConfiguration { ScaffoldAllTables = true });
}
public void BuildColumns(DataGrid targetGrid)
{
MetaTable metaTable = model.GetTable(typeof(TEntity));
// Decision whether to auto-generated columns still rests with the caller.
targetGrid.Columns.Clear();
foreach (var metaColumn in metaTable.Columns.Where(x => x.GetType().Name == "MetaColumn" && x.Scaffold))
{
switch (metaColumn.ColumnType.Name)
{
case "Boolean":
targetGrid.Columns.Add(new DataGridCheckBoxColumn { Binding = new Binding(metaColumn.Name), Header = metaColumn.DisplayName });
break;
default:
targetGrid.Columns.Add(new DynamicDataGridTextColumn { MetaColumn = metaColumn, Binding = new Binding(metaColumn.Name), Header = metaColumn.DisplayName });
break;
}
}
}
}
TContext is the type of your object model, and TEntity the type of the entity / class in that model your want to generate controls for.
Use control data binding. It will do all the work for you.