Ok, a bit of a strange one - and it's probably something simple as I'm pretty new to Silverlight!
I have an object with the following property:-
private int targetID = NULL_TARGET_VALUE;
[Display(Name="Target", Order=1)]
[Required]
public int TargetID
{
get
{
return targetID;
}
set
{
if (this.targetID != value)
{
this.ValidateProperty("TargetID", value);
this.targetID = value;
this.RaisePropertyChanged("TargetID");
}
}
}
This object is created using the DataForm from the toolkit. I use the AutoGeneratingField event to change the item to a combo box drop down with the code below:
if (e.PropertyName == "TargetID")
{
ComboBox target = new ComboBox() { DisplayMemberPath = "Title", SelectedValuePath = "ItemID" };
target.ItemsSource = TaskManager.Manager.GanttItemSource;
var selectedItem = TaskManager.Manager.GanttItemSource.FirstOrDefault(p => p.ItemID == ParentTargetID);
target.SelectedItem = selectedItem;
e.Field.ReplaceTextBox(target, ComboBox.SelectedValueProperty, binding => binding.Converter = new TargetNullValueConverter());
break;
}
This does result in a drop down as I would expect. On my save button event I have this code:
if (registerForm.ValidateItem())
{
this.task.Save();
}
If the debugger is attached to the silverlight project this works great. If it's not then ValidateItem returns false as it thinks I have added an invalid target ("Input is not in a correct format" is the exact validation error I get).
Any ideas really appreciated! (BTW Just to confirm this happens in both release and debug build modes, simply attaching or removing a debugger causes this to occur)
Dammit, the issues was down to this line in the autogeneratingfield event:-
e.Field.ReplaceTextBox(target, ComboBox.SelectedValueProperty, binding => binding.Converter = new TargetNullValueConverter());
This allows it to work
e.Field.ReplaceTextBox(target, ComboBox.SelectedValueProperty);
Which makes sense because I'm dealing with ints not objects. Still don't know why it works with the debugger attached however!
Related
I have a ComboBox (ToolStripCombobox, to be more precise) filled with items of type KeyValuePair<Int32, FontFamily>. I managed to have the Items beeing painted manually by using the DrawItem event. So every Item is painted with the FontFamily of the corresponding KeyValuePair. This works fine for the DropDownList, but when I select an Item out of the List and the list closes, the text in the ComboBox says something like "[21, [FontFamily: Name=Arial]]" which is most likely the result of SelectedItem.ToString().
Any ideas how to solve this problem?
here is the code of my custom DrawItem method:
private void fontComboBoxDrawItem(object sender, DrawItemEventArgs e)
{
e.DrawBackground();
if ((e.State & DrawItemState.Focus) != 0)
{
e.DrawFocusRectangle();
}
Brush objBrush = null;
var itemToDraw = this.fontComboBox.Items[e.Index];
KeyValuePair<Int32, FontFamily> windowsFontItem = (KeyValuePair<Int32, FontFamily>)itemToDraw;
objBrush = new SolidBrush(e.ForeColor);
e.Graphics.DrawString(windowsFontItem.Value.Name, new Font(windowsFontItem.Value, e.Font.Size), objBrush, e.Bounds);
if (objBrush != null)
{
objBrush.Dispose();
}
objBrush = null;
}
Update:
It works as expected, when I set the DropDownStyle of the ComboBox to ComboBoxStyle.DropDownList
But I´d rather use ComboBoxStyle.DropDown, so you can edit the Text to search for Fonts.
Summary:
I'm creating a simple application for practice, where the user can maintain and query a collection of Things. On the UI are several TextBoxes and ComboBoxes with which they can filter the collection.
The three buttons I'm concerned with are [Filter], [Random], and [All]. [Filter] applies the current filter options. [Random] applies the current filter options (if any), and then only shows one random entry from the filtered results. [All], as expected, shows the unfiltered collection.
To fully understand the background for the question, I'll provide the relevant code.
Here is where anything having to do with the CollectionViewSource (or any relevant code I'm posting) gets declared:
//Members
private ObservableCollection<Thing> _myDataCollection;
private CollectionViewSource _CVS;
private Thing _randomThing;
//Properties
public ObservableCollection<Thing> MyDataCollection
{
get { return _myDataCollection; }
set
{
if (_myDataCollection!= value)
{
_myDataCollection= value;
RaisePropertyChanged(() => MyDataCollection);
}
}
}
public CollectionViewSource CVS
{
get { return _CVS; }
set
{
if (_CVS != value)
{
_CVS = value;
RaisePropertyChanged(() => CVS);
}
}
}
public ICollectionView CVSView
{
get { return CVS.View; }
}
Here is where the CVS is initialized (in the window view-model's constructor). For now, the data collection is populated with a ton of random things (that's all that RandomizeData() does).
MyDataCollection = new ObservableCollection<Thing>();
RandomizeData();
CVS = new CollectionViewSource();
CVS.Source = MyDataCollection;
Here is the code for the [Filter] button's command:
//Clear any stale filter options and rebuild with the most current ones.
CVSView.Filter = null;
//IF THE FOLLOWING LINE IS UNCOMMENTED, THE ISSUE OCCURS.
//CVS.Filter -= new FilterEventHandler(SingleRandomFromCollectionFilter);
BuildCVSFilter();
The code for the [All] button literally just clears the filter:
CVSView.Filter = null;
The code for the [Random] button. I suspect the issue is coming from the handler added here.
//Clear any stale filter options and rebuild with the most current ones.
CVSView.Filter = null;
//IF THE FOLLOWING LINE IS UNCOMMENTED, THE ISSUE OCCURS.
//CVS.Filter -= new FilterEventHandler(SingleRandomFromCollectionFilter);
BuildCVSFilter();
//Only proceed if there are actually results in the filtered collection.
int resultsCount = CVSView.Cast<Thing>().Count();
if (resultsCount > 0)
{
//Point to a random thing in the filtered collection.
CVSView.MoveCurrentToPosition(random.Next(0, resultsCount));
_randomThing = CVSView.CurrentItem as Thing;
//Add another filter event that further constrains the collection to only contain the random thing.
CVS.Filter += new FilterEventHandler(SingleRandomFromCollectionFilter);
}
And here is the code for that BuildCVSFilter() I've been calling. I use this so that I can use multiple filters concurrently. The "FilterOption" strings are properties that are bound to the values of the UI controls.
if (!string.IsNullOrEmpty(FilterOption1))
{
CVS.Filter += new FilterEventHandler(Fitler1);
}
if (!string.IsNullOrEmpty(FilterOption2) && FilterOption2 != "ignore")
{
CVS.Filter += new FilterEventHandler(Fitler2);
}
if (!string.IsNullOrEmpty(FilterOption3))
{
CVS.Filter += new FilterEventHandler(Filter3);
}
Each filter that gets added this way only sets e.Accepted to false, if applicable, and leaves it alone if it's true.
Issue:
If I click on [Random] at all, it seems like the FilterEventHandler that gets added there does not go away. This makes it so that selecting [Filter] after [Random] won't work as expected, because it's only going to filter from the current collection of one thing. Additionally, selecting [Random] a second time seems to reuse the same random thing (instead of finding a new one). Interestingly enough, clicking [All] still works just fine. It shows everything.
When I go into those [Filter] and [Random] OnCommand methods and explicitly add a line to remove that SingleRandomFromCollectionFilter handler, everything works as expected.
Why would NameEntriesView.Filter = null; work to clear the filter on [All], but not on [Filter] or [Random]? Is there something about the CollectionViewSource and its implementation that I'm not fully understanding?
I'm looking at ways to resolve an issue with a Winforms application, which uses a ComboBox control. Specifically, the ComboBox (Style=DropDownList) is bound to a datasource and, as the user navigates through some other data, the "Text" property of the ComboBox property is set - and the user can select some other value.
The trouble starts when the value I set the "Text" property to is not in the list of available items. It seems that nothing happens. Take the following simple example:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
myComboBox1.DropDownStyle = ComboBoxStyle.DropDownList;
//myComboBox1.Items.AddRange(new[] { "One", "Two", "Three" });
List<KeyValuePair<Int32, String>> values = new List<KeyValuePair<Int32, String>>();
values.Add(new KeyValuePair<Int32, String>(1, "One"));
values.Add(new KeyValuePair<Int32, String>(2, "Two"));
values.Add(new KeyValuePair<Int32, String>(3, "Three"));
myComboBox1.DataSource = values;
myComboBox1.ValueMember = "Key";
myComboBox1.DisplayMember = "Value";
button1.Click += (s, e) => { myComboBox1.Text = "Four"; };
button2.Click += (s, e) => { myComboBox1.SelectedIndex -= 1; };
}
}
public class MyComboBox : System.Windows.Forms.ComboBox
{
public override string Text
{
get { return base.Text; }
set { MessageBox.Show(value); base.Text = value; }
}
}
This technique is used throughout a large application, so when it was noticed that (in the example above) setting the "Text" to "Four" does nothing, I thought that maybe I could trap this and throw an exception. In reality, the application is peppered with code like this:
if (myDataRow.IsBlahNull())
myComboBox1.Text = "";
else
myComboBox1.Text = myDataRow.Blah;
Now, while I appreciate that setting "SelectedIndex = -1" would be better for the "IsNull" case, the fact remains that myDataRow.Blah may not be a valid value. Also, the application is written (and live) so the fewer changes the better.
So, my immediate thought was "let's override the Text property setter and check that the value is in the list". That, it turns out, is nothing like as simple as it would seem. The problem being that the "Text" property is set to all kinds of things, in all kinds of scenarios. For example, it's set when the DataSource property is assigned, or when the SelectedIndex is set to -1. Also, it's set to the string representation of the selected item - so if you happen to have a ComboBox control that's bound to a List of KeyValue pairs, you get the "Text" property set to something like "[Key,Value]". If it's bound to a DataTable/DataView, you get the string representation of the DataRow, and that gets even harder to detect.
It's at this point I thought that there might be another way to achieve the desired result (which is to detect the setting of the Text property to some invalid value - which does nothing).
Any ideas ?
Upon reflection, is this a reasonable work-around ?
/// <summary>
/// Gets or sets the text associated with this control.
/// </summary>
public override string Text
{
get { return base.Text; }
set
{
base.Text = value;
if ((value != null) && (base.Text != value))
if (value == "")
this.SelectedIndex = -1;
else
throw new ArgumentException(String.Format("Cannot set Text property of {0} to \"{1}\".", this.Name, value));
}
}
I have a WPF DataGrid control with a SelectionUnit of "FullRow" and SelectionMode of "Extended" that I'm programmatically selecting an item in (the first item, usually). The selection works, but for some reason any form of programmatic selection seems to break the shift-select multiselect ability.
If I single click another item in the DataGrid (so the item I just clicked is the only item selected), then shift-select will work. It only seems to break if I've programmatically selected the item. Additionally, control-click works to select multiple items in either case -- it seems to only be shift-select that is broken.
I've tried various forms of programmatically selecting the single item, from as simple as myGrid.SelectedIndex = 0, to using the DataGrid's ItemContainerGenerator to get an instance of the DataGridRow object and setting IsSelected = true on it, but to no avail.
To re-iterate -- programmatic selection of an item works, but it breaks shift-click selection.
Has anyone run into this before? I've tried setting focus on the DataGridRow instance that is programmatically selected, but it doesn't seem to help?
I succeeded to work around this problem using reflection:
var method = typeof(DataGrid).GetMethod("HandleSelectionForCellInput", BindingFlags.Instance | BindingFlags.NonPublic);
method.Invoke(MyDataGrid, new object[] { cellToSelect, false, false, false });
I struggled with this problem for multiple days and tried a lot of things that I found on the internet. In the end, I found the solution that works for me by studying the source code of the DataGrid.
In the DataGrid I noticed a member variable called _selectionAnchor and guessed that this must be the starting point for when a user expands the selection in the grid. My solution is to set this member to the first cell of the row that is selected. If a row is selected in code, than this fix makes sure that when expanding the selection it starts at the selected row.
Please note that I used the code from this issue to enable multiselect. Then, in file MainWindow.xaml.cs, I added this code:
private void ExampleDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (ExampleDataGrid.SelectedItems.Count > 0)
{
ExampleDataGrid.ScrollIntoView(ExampleDataGrid.SelectedItems[0]);
// Make sure that when the user starts to make an extended selection, it starts at this one
foreach (var cellInfo in ExampleDataGrid.SelectedCells)
{
if (cellInfo.Column.DisplayIndex == 0)
{
var cell = GetDataGridCell(cellInfo);
cell?.Focus();
var field = typeof(DataGrid).GetField("_selectionAnchor", BindingFlags.NonPublic | BindingFlags.Instance);
field?.SetValue(ExampleDataGrid, cellInfo);
break;
}
}
}
}
public DataGridCell GetDataGridCell(DataGridCellInfo cellInfo)
{
var cellContent = cellInfo.Column.GetCellContent(cellInfo.Item);
if (cellContent != null)
{
return (DataGridCell)cellContent.Parent;
}
return null;
}
In the xaml file:
<vm:CustomDataGrid x:Name="ExampleDataGrid" ItemsSource="{Binding ImportItems}"
SelectedItemsList="{Binding SelectedImportItems, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
AutoGenerateColumns="False" SelectionMode="Extended" IsReadOnly="True" CanUserAddRows="False"
SelectionChanged="ExampleDataGrid_SelectionChanged">
Remember there is a difference between focus and keyboard focus. When you select the item in code, check to see what control has Keyboard focus / regular focus. I'm guessing that the data grid loses this focus until you click on it with the mouse and then it regains the focus needed to use the ctrl function.
I ran into this issue in a WPF user control we were hosting inside a C++ application.
I just resolved exactly the same problem with the help of #ezolotko's snippet.
Because the grid is dynamically generating rows I needed to subscribe to ItemContainerGenerator.StatusChanged event and find the first cell in a row representing this element.
To find the cell I used DataGridHelper class and wrapped it all in an attached behaviour:
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using Speedwell.WPF.Helpers;
namespace Speedwell.WPF.Behaviors
{
public static class DataGridSingleRowSelected
{
public static readonly DependencyProperty IsSelectionFixEnabledProperty = DependencyProperty.RegisterAttached
(
"IsSelectionFixEnabled",
typeof(bool?),
typeof(DataGridSingleRowSelected),
new PropertyMetadata(null, IsSelectionFixEnabledChanged)
);
public static bool GetIsSelectionFixEnabled(DataGrid element)
{
return (bool)element.GetValue(IsSelectionFixEnabledProperty);
}
public static void SetIsSelectionFixEnabled(DataGrid element, bool value)
{
element.SetValue(IsSelectionFixEnabledProperty, value);
}
private static void IsSelectionFixEnabledChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
var dataGrid = sender as DataGrid;
if(dataGrid != null)
{
if(args.OldValue == null)
{
dataGrid.ItemContainerGenerator.StatusChanged += (s, e) => ContainerStatusChanged(dataGrid, ((ItemContainerGenerator)s));
}
}
}
private static void ContainerStatusChanged(DataGrid dataGrid, ItemContainerGenerator generator)
{
if(generator != null && generator.Status == GeneratorStatus.ContainersGenerated && dataGrid.SelectedItems.Count == 1)
{
var row = (DataGridRow)dataGrid.ItemContainerGenerator.ContainerFromItem(dataGrid.SelectedItems[0]);
if(row != null)
{
var cell = dataGrid.GetCell(row, 0);
if(cell != null)
{
SelectCellMethod.Invoke(dataGrid, new object[] { cell, false, false, false });
}
}
}
}
private static readonly MethodInfo SelectCellMethod = typeof(DataGrid).GetMethod("HandleSelectionForCellInput", BindingFlags.Instance | BindingFlags.NonPublic);
}
}
As you can see the proper selection is only applied when there is a single (1) row selected and this is exactly what I need and it seems it also what #Jordan0Day requested.
This really seems like a bug to me, but perhaps some databinding gurus can enlighten me? (My WinForms databinding knowledge is quite limited.)
I have a ComboBox bound to a sorted DataView. When the properties of the items in the DataView change such that items are resorted, the SelectedItem in my ComboBox does not keep in-sync. It seems to point to someplace completely random. Is this a bug, or am I missing something in my databinding?
Here is a sample application that reproduces the problem. All you need is a Button and a ComboBox:
public partial class Form1 : Form
{
private DataTable myData;
public Form1()
{
this.InitializeComponent();
this.myData = new DataTable();
this.myData.Columns.Add("ID", typeof(int));
this.myData.Columns.Add("Name", typeof(string));
this.myData.Columns.Add("LastModified", typeof(DateTime));
this.myData.Rows.Add(1, "first", DateTime.Now.AddMinutes(-2));
this.myData.Rows.Add(2, "second", DateTime.Now.AddMinutes(-1));
this.myData.Rows.Add(3, "third", DateTime.Now);
this.myData.DefaultView.Sort = "LastModified DESC";
this.comboBox1.DataSource = this.myData.DefaultView;
this.comboBox1.ValueMember = "ID";
this.comboBox1.DisplayMember = "Name";
}
private void saveStuffButton_Click(object sender, EventArgs e)
{
DataRowView preUpdateSelectedItem = (DataRowView)this.comboBox1.SelectedItem;
// OUTPUT: SelectedIndex = 0; SelectedItem.Name = third
Debug.WriteLine(string.Format("SelectedIndex = {0:N0}; SelectedItem.Name = {1}", this.comboBox1.SelectedIndex, preUpdateSelectedItem["Name"]));
this.myData.Rows[0]["LastModified"] = DateTime.Now;
DataRowView postUpdateSelectedItem = (DataRowView)this.comboBox1.SelectedItem;
// OUTPUT: SelectedIndex = 2; SelectedItem.Name = second
Debug.WriteLine(string.Format("SelectedIndex = {0:N0}; SelectedItem.Name = {1}", this.comboBox1.SelectedIndex, postUpdateSelectedItem["Name"]));
// FAIL!
Debug.Assert(object.ReferenceEquals(preUpdateSelectedItem, postUpdateSelectedItem));
}
}
To clarify:
I understand how I would fix the simple application above--I only included that to demonstrate the problem. My concern is how to fix it when the updates to the underlying data rows could be happening anywhere (on another form, perhaps.)
I would really like to still receive updates, inserts, deletes, etc. to my data source. I have tried just binding to an array of DataRows severed from the DataTable, but this causes additional headaches.
Just add a BindingContext to the ComboBox :
this.comboBox1.DataSource = this.myData.DefaultView;
this.comboBox1.BindingContext = new BindingContext();
this.comboBox1.ValueMember = "ID";
this.comboBox1.DisplayMember = "Name";
By the way, try not keeping auto-generated names for your widgets (comboBox1, ...), it is dirty. :-P
The only promising solution I see at this time is to bind the combo box to a detached data source and then update it every time the "real" DataView changes. Here is what I have so far. Seems to be working, but (1) it's a total hack, and (2) it will not scale well at all.
In form declaration:
private DataView shadowView;
In form initialization:
this.comboBox1.DisplayMember = "Value";
this.comboBox1.ValueMember = "Key";
this.shadowView = new DataView(GlobalData.TheGlobalTable, null, "LastModified DESC", DataViewRowState.CurrentRows);
this.shadowView.ListChanged += new ListChangedEventHandler(shadowView_ListChanged);
this.ResetComboBoxDataSource(null);
And then the hack:
private void shadowView_ListChanged(object sender, ListChangedEventArgs e)
{
this.ResetComboBoxDataSource((int)this.comboBox1.SelectedValue);
}
private void ResetComboBoxDataSource(int? selectedId)
{
int selectedIndex = 0;
var detached = new KeyValuePair<int, string>[this.shadowView.Count];
for (int i = 0; i < this.shadowView.Count; i++)
{
int id = (int)this.shadowView[i]["ID"];
detached[i] = new KeyValuePair<int, string>(id, (string)this.shadowView[i]["Name"]);
if (id == selectedId)
{
selectedIndex = i;
}
}
this.comboBox1.DataSource = detached;
this.comboBox1.SelectedIndex = selectedIndex;
}
Must detach event handler in Dispose:
this.shadowView.ListChanged -= new ListChangedEventHandler(shadowView_ListChanged);
Your example sorts the data on the column it updates. When the update occurs, the order of the rows changes. The combobox is using the index to keep track of it's selected items, so when the items are sorted, the index is pointing to a different row. You'll need to capture the value of comboxBox1.SelectedItem before updating the row, and set it back once the update is complete:
DataRowView selected = (DataRowView)this.comboBox1.SelectedItem;
this.myData.Rows[0]["LastModified"] = DateTime.Now;
this.comboBox1.SelectedItem = selected;
From an architecture perspective, the SelectedItem must be cleared when rebinding the DataSource because the DataBinder don't know if your SelectedItem will persist or not.
From a functional perspective, the DataBinder may not be able to ensure that your SelectedItem from you old DataSource is the same in your new DataSource (it can be a different DataSource with the same SelectedItem ID).
Its more an application feature or a custom control feature than a generic databinding process.
IMHO, you have theses choices if you want to keep the SelectedItem on rebind :
Create a reusable custom control / custom DataBinder with a persistance option which try to set the SelectedItem with all your data validation (using a DataSource / item identification to ensure the item validity)
Persist it specifically on your Form using the Form/Application context (like ViewState for ASP.NET).
Some controls on the .NET market are helping you by rebinding (including selections) the control from their own persisted DataSource if the DataSource is not changed and DataBind not recalled. That's the best pratice.