Implement INotifyPropertyChanged on a list of objects? - c#

Hmm, not sure if I chose the right approach.
I have a grid of components. In the first column there are DatePickers.
In the second column there are combo-boxes. In the last column there are text-boxes. The grid has 15 rows.
I named them by their column and row number as you would number cells in a grid.
So dp1_1 for DatePicker are position (1,1), dp2_1 for position (2,1).
cb1_1 for ComboBox are position (1,1), cb2_1 for ComboBox position (2,1).
I keep my date pickers data, combo-boxes data, text-boxes data in an ordinary list for easy access/reference, like so:
public int numOfRows = 15;
private List<DateTime> _MyDateTimeList = new List<DateTime>();
public List<DateTime> MyDateTimeList
{
get { return _MyDateTimeList; }
set {
DateTime pomDatumObjava;
_MyDateTimeList = value;
for (int i = 0; i < numOfRows; i += 1)
{
pomDatumObjava = new DateTime();
// code for accessing/enabling/disabling the appropriate date picker, which doesn't work since I don't know how to send the window reference where my date pickers reside
// pomDatumObjava = Utils.enableDisableDatePicker(null, Constants.DP_LABEL + stringIndex, true, 1).SelectedDate.Value;
_MyDateTimeList.Add(pomDatumObjava);
}
OnPropertyChanged("MyDateTimeList");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string name)
{
Console.WriteLine("OnPropertyChanged -> " + name);
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
Console.WriteLine("handler != null -> " + name);
handler(this, new PropertyChangedEventArgs(name));
}
}
public static DatePicker enableDisableDatePicker(System.Windows.Window myWindow, string name, bool enableDisable, double op)
{
DatePicker dp = (DatePicker)myWindow.FindName(name);
if (!enableDisable)
{
dp.SelectedDate = null;
}
dp.IsEnabled = enableDisable;
dp.Opacity = op;
return dp;
}
How do I access my components in the window and reference them appropriately so that each time I change a value in certain DatePicker, I detect the change in the list?
You can find the Utils function in comments line. Where it says null, there should be the window objects where my components are placed.
Or, is this the right approach?
I will have lot of components(15x3 = 45 x code for OnPropertyChanged), so the MVVM file will be quite large to set OnPropertyChanged() for all of them.

As ASh says, you need an ObservableCollection of objects, one for each row. ObservableCollections automatically update their bound controls when you add or remove objects, and pass on events when the objects change. These objects would presumably have three properties (for the datepicker, combobox and text box) that have OnPropertyChanged().
Then bind the ObservableCollection to the ItemSource of your grid, and the three controls to the three properties of an item.
For MVVM, you shouldn't ever need to reference a control in the View. Instead the view should reflect the state of the ViewModel. If you want to disable a datepicker, it's Enabled property should be bound to some thing that raises OnPropertyChanged().
If you post your view, we can suggest how to do this.

Related

How to increase a ProgressBar Value according to the content of TextBoxes?

So I have 4 TextBoxes and I have already set the maximum value of my ProgressBar to 4.
ProgressAttr.Maximum = 4;
What I would like to do is increase my ProgressBar Value by 1 every time I fill out a TextBox.
My code right now looks like this:
if (!string.IsNullOrEmpty(Name_txtBox.Text))
{
ProgressAttr.Value += 1;
}
if (!string.IsNullOrEmpty(Serial_TxtBox.Text))
{
ProgressAttr.Value += 1;
}
if (!string.IsNullOrEmpty(Cap_TxtBox.Text))
{
ProgressAttr.Value += 1;
}
if (!string.IsNullOrEmpty(IDprk_TxtBox.Text))
{
ProgressAttr.Value += 1;
}
This doesn't increase the value of my ProgressBar.
I've also tried this:
if (textbox.Text.Length > 0)
{
ProgressAttr.Value += 1;
}
None of this works for me and Ive been trying to find a solution for hours. I would really appreciate your help and am looking forward to seeing solutions that you guys suggest!
I'm proposing you a method that makes use of DataBindings to synchronize the content of your TextBoxes with the Value property of a ProgressBar.
A class object can notify changes related to its Properties values implementing the INotifyPropertyChanged interface. Its public PropertyChanged event is raised to notify bound Controls that Properties of the data Provider have changed.
All bound Properties are then updated to the new values.
This allows you to have all the logic in a single place and changes to the User Interface (your Form, here) do not affect the data binding in any way.
You can add or remove Controls from the UI. The binding procedure doesn't change or needs to keep track of what has changed in the UI.
For example, bind your ProgressBar.Value property to the ProgressBarController.Value property. You initialize the ProgressBarController with the instances of the TextBox (or RichTextBox) Controls that you want to include, add a Binding to link the properties and that's all. All the rest happens automatically.
ProgressBarController pbarController = null;
// Form Constuctor
public SomeForm()
{
InitializeComponent();
// [...]
// These TextBoxes could be child of a Container (e.g., a Panel), so you could
// also get all the child Controls of this Container to build the array
var textBoxes = new[]{ Name_txtBox, Serial_TxtBox, Cap_TxtBox, IDprk_TxtBox}
ProgressAttr.Maximum = textBoxes.Length;
pbarController = new ProgressBarController(textBoxes);
ProgressAttr.DataBindings.Add("Value", pbarController, "Value", false,
DataSourceUpdateMode.OnPropertyChanged);
}
protected override void OnFormClosed(FormClosedEventArgs e)
{
pbarController.Dispose();
base.OnFormClosed(e);
}
Here, two TextBoxes already contain some text when the Form is loaded, so the ProgressBar shows a progress. If you remove all text in the Designer, of course the progress shown is initially 0:
The ProgressBarController class is initialized with the array of Controls passed in its Contructor.
► It then build a Dictionary<TextBoxBase, int> to keep track of the progress value associated to a Control: 0 if its Text is empty, otherwise 1.
TextBoxBase so you can also use RichTextBox Controls.
► The TextChanged event of these Controls is subscribes to using a single handler. The sender object will be the Control that raised the event.
► If/when the associated value has changed (the Control Text state determines a change), the PropertyChanged event is raised and the DataBinding notifies the ProgressBar to update its Value property.
► When the Parent Form is closed, call the Dispose() method of this class to remove the subscription to the TextChanged events.
using System.Runtime.CompilerServices;
private class ProgressBarController : INotifyPropertyChanged, IDisposable
{
public event PropertyChangedEventHandler PropertyChanged;
private Dictionary<TextBoxBase, int> states;
private int m_Value = 0;
public ProgressBarController(params TextBoxBase[] tboxes) {
states = new Dictionary<TextBoxBase, int>();
for (int i = 0; i < tboxes.Length; i++) {
states.Add(tboxes[i], tboxes[i].Text.Length > 0 ? 1 : 0);
tboxes[i].TextChanged += TextChanged;
}
m_Value = states.Values.Sum();
}
public int Value {
get => m_Value;
private set {
if (value != m_Value) {
m_Value = value;
OnPropertyChanged();
}
}
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = "") =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
protected void TextChanged(object sender, EventArgs e)
{
var tbx = sender as TextBoxBase;
int state = tbx.Text.Length > 0 ? 1 : 0;
if (states[tbx] != state) {
states[tbx] = state;
Value = states.Values.Sum();
}
}
public void Dispose() {
foreach (var tb in states.Keys) {
tb.TextChanged -= this.TextChanged;
}
}
}

Responding to object property changes within a class object hierarchy

I am developing an application which uses a hierarchical object structure and displays a few key object properties from those objects on the main GUI within a DataGridView. Those values must update when the underlying data changes. I have considered a few options:
Bind the individual DataGridView cells to the relevant object properties. I understand that this is not possible, and DGV binding is all or nothing.
Dynamically position Textboxes over the grid cells and bind those, but this seems messy.
Create an intermediate list/array/collection which references only the relevant object properties, and then use that list as a data source for the DataGridView.
Respond to the PropertyChanged events. The complication is that I have got multiple classes. The top-level object exists within the UI scope, and has a child object which in turn may have multiple child objects of its own, and so on. The UI can access properties of all objects, but not their events.
I have been looking at passing the PropertyChanged event from whichever level it occurs up the chain so that it can be handled within the UI. So within a particular class I want to respond to OnPropertyChanged within that class, and within any children, and make any events raised available to the parent class. Thus events would flow up the tree to the top.
I understand how to do the two steps individually, I think, with reference to the following:
Handling OnPropertyChanged
Pass click event of child control to the parent control
However, although I presume the two can be combined, I am not quite sure how to do this. In the UI I have got this:
project.PropertyChanged += new PropertyChangedEventHandler(ProjectPropertyChanged);
private void ProjectPropertyChanged(object sender, PropertyChangedEventArgs e) {
MessageBox.Show("In main UI. Project property changed!");
}
And then one level down I have got something like this:
public class Project : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public Project() {
childObject.PropertyChanged += new PropertyChangedEventHandler(ProjectPropertyChanged);
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
ProjectPropertyChanged(sender, e); // this doesn't work due to different parameters
}
private void PropertyChanged(object sender, PropertyChangedEventArgs e)
{
// Event available to parent class
}
}
The idea being that each class would pass its own OnPropertyChanged() events to its PropertyChanged() method, and respond to its children's OnPropertyChanged() events, and expose all to the parent class.
If doing this, ideally I would like to retain knowledge of which property changed in order to respond accordingly.
The most immediate issue is lack of compatibility between ProjectPropertyChanged and OnPropertyChanged due to different parameters. More fundamentally, though, I am not sure whether this method is workable or optimal.
How best to do this?
To answer my own question:
I tried unsuccessfully to do this with an intermediate binding source, using a DataTable (as per this question.). The problem was that I couldn't create references to the data objects. The DataTable seemed to contain values.
So I ended up using a method I was more sure about, but is less elegant, which is option 2 in my question above. I position Labels where I need bound data values, and bind to those labels. This works.
With some simplification, and pretending that our objects are animals, my solution was this:
Label[,] dashboardLabels = new Label[3,14];
private void Form1_Shown(object sender, EventArgs e)
{
CreateLabels(); // create and position labels (no binding yet)
}
public void CreateLabels(int cols = 3, int rows = 14)
{
for (int col = 0; col < cols; col++)
{
for (int row = 0; row < rows; row++)
{
// Create label...
Label l = new Label();
l.Text = "N/A";
l.ForeColor = Color.Red
dashboardLabels[col, row] = l;
this.Controls.Add(l);
// Position label over DGV cell...
Point dgvCell = dataGridView1.GetCellDisplayRectangle(col + 2, rowNumbersLabel[row], false).Location;
Point dgvGrid = dataGridView1.Location;
l.Left = dgvGrid.X + dgvCell.X;
l.Top = dgvGrid.Y + dgvCell.Y;
}
}
}
private void UpdateLabels(List<Dog> dogs)
{
for (int i = 0; i < dogs; i++)
{
if (!dashboardLabels[i, 0].Visible) dashboardLabels[i, 0].Visible = true;
if (dogs[i].IsSetUp) BindLabel(dashboardLabels[i, 0], dogs[i],"Name");
}
}
private void BindLabel(Label l, Dog dog, string observation)
{
Binding b = new Binding("Text", dog, observation);
l.DataBindings.Add(b);
l.ForeColor = Color.Green;
}
}
Then when the objects are created, I call UpdateLabels(). If not initialised, the label will show 'N/A' in red at this point. If initialised, the label will become green and will be bound to the object's name so it will update automatically from that point on.
I did much searching and the information I was finding suggested that a DataGridView does not support complex data binding i.e. it is pretty much one class to one DGV, or not at all. I couldn't find an alternative grid-like control which would do it either.

C# inline sorting ObservableCollection does not update Data Binding

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.

Telerik RadGauge + Data Binding

So I'm trying to update my RadRadialGauge. It will display data (using an animated Needle) that is being retrieved on a real-time basis. I have a RadChartView that currently works by using TimeStamp and Value properties to draw the Chart. When I add a chart, sometimes I might want to add a few based on the variable I'm watching. For instance, if I want to watch Motor Speed and Output Frequency, I have to add multiple vertical axes. Part of my code to handle the data binding for the RadChartView is here:
var lineSeries = new LineSeries();
lineSeries.CategoryBinding =
new PropertyNameDataPointBinding() { PropertyName = "TimeStamp" };
lineSeries.ValueBinding =
new PropertyNameDataPointBinding() { PropertyName = "Value" };
lineSeries.ItemsSource = (chart.Chart as GuiAnalogQueue).Records;
The rest of the code is just appearance handling, and then at the end I add the LineSeries to my RadChartView.
Now, I'm trying to port this code, in a way, over to RadGauge. I'm not sure how to bind the values to the Needle so the needle moves when the Value changes.
In the XAML I've tried Value="{Binding Value}" I've tried adding binding to the ValueSource varible. Also I have done needle.Value = chart.Chart.Value;
I can't figure it out, so any help is appreciated.
UPDATE
This is what I'm trying to accomplish. My Records collection has two properties, Value and TimeStamp. I'm trying to bind my Value in the Records to the needle Value. This is my approach to do it programmatically:
public void InitializeCharts(ChartsVM charts, Theme theme)
{
DataContext = charts;
foreach (cVM chart in charts.Charts)
{
Binding value = new Binding();
value.Source = (chart.Chart as GuiAnalogQueue).Records;
value.Path = new PropertyPath("Value");
needle.SetBinding(Needle.ValueProperty, value);
}
}
However, when I do this, it is not changing the needle.Value at all. My Records is the collection that uses NotifyPropertyChanged("Records"), so I would expect my needle to change everytime Records is changed.
As you see in my original post, those three lines take care of binding the variables to ChartView charts, however I can't get the RadGauge to work.
In short, I found that Needle's don't use any type of collections for their Values. So when I tried setting up a Source to be inside of a collection, and a Path, it wasn't really liking that. Instead, I added a property right before I add the Value to the records collection (in my update values function). That way I could set my binding up as:
Binding value = new Binding();
value.Source = (chart.Chart as GuiAnalogQueue);
value.Path = new PropertyPath("AnalogValue");
needle.SetBinding(Needle.ValueProperty, value);
That reads as, the Needle will bind its Value property with the AnalogValue property that is in the Source--chart.Chart as GuiAnalogQueue.
Hope this helps if you've been directed to this page.
Here is a basic example of using the RadRadialGauge.
XAML:
<telerik:RadRadialGauge x:Name="radialGauge"
Width="300"
Height="300"
Grid.Column="0">
<telerik:RadialScale Min="1"
Max="12">
<telerik:RadialScale.Indicators>
<telerik:Needle x:Name="needle" />
<telerik:Pinpoint/>
</telerik:RadialScale.Indicators>
</telerik:RadialScale>
</telerik:RadRadialGauge>
As you can see i have a radial gauge with a radial scale defined. Radial Scale has a needle as the indicator. The RadialScale is from 1 to 12. Note that i have given a name for the needle. We will use this to push values from the code behind.
In this example i am using a dispatcher timer to tick every 1 second and i am generating a random value between 1 to 12. Here is the code behind snippets.
code snippet:
Following variables are declared at the window level
TimeSpan interval = TimeSpan.FromSeconds(1);
DispatcherTimer timer;
Random rnd = new Random();
I have defined event handlers for Window Loaded & Unloaded events. On Window Load, i start the timer.
void OnWindowLoad(object sender, RoutedEventArgs e)
{
timer = new DispatcherTimer();
timer.Interval = interval;
timer.Tick += new EventHandler(Timer_Tick);
timer.Start();
}
Here is the timer tick function:
private void Timer_Tick(object sender, EventArgs e)
{
timer.Stop();
SetNextValue();
timer.Start();
}
Here is the SetNextValue function:
private void SetNextValue()
{
int minValue = 1;
int maxValue = 12;
int nextValue = rnd.Next(minValue, maxValue);
needle.Value = nextValue;
}
In the Unloaded event handler i am stopping the timer.
void OnWindowUnload(object sender, RoutedEventArgs e)
{
timer.Stop();
}
Output:
when you run the app, you will see the needle changing its position because we are generating random numbers from 1 to 12 every second and we set the generated number to needle's value. The SetNextValue() method can be your gateway to monitoring the real value and set the needle value to that real data.
This is the basic example code i can think of to explain the radial gauge.
Hope this provides the answer you are looking for.
Update:
Here is an MVVM way of setting the needle value. Let the window implement INotifyPropertyChanged interface. Set the datacontext to the window itself
public partial class MainWindow : Window, INotifyPropertyChanged
public MainWindow()
{
InitializeComponent();
Loaded += OnWindowLoad;
Unloaded += OnWindowUnload;
DataContext = this;
}
Provide implementation for the PropertyChanged event like below:
public event PropertyChangedEventHandler PropertyChanged = delegate { };
void NotifyPropertyChanged(string propName)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
Implement a property called NeedleValue.
int needleValue = 1;
public int NeedleValue
{
get
{
return needleValue;
}
set
{
needleValue = value;
NotifyPropertyChanged("NeedleValue");
}
}
In the SetNextValue - just set the newly created NeedleValue property. this will fire the property changed notification.
private void SetNextValue()
{
int minValue = 1;
int maxValue = 12;
int nextValue = rnd.Next(minValue, maxValue);
NeedleValue = nextValue;
}
In the XAML bind the Needle Value property to NeedleValue like below
<telerik:Needle x:Name="needle" Value="{Binding NeedleValue}" />
Hope this provides you with the answer you are looking for :)
Lohith (Tech Evangelist, Telerik India)

Setting controls in a Telerik GridView cell

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

Categories