Currently I'm developing .net 4.5 wpf MVVM application with validation system handled by INotifyDataErrorInfo. At some point in application I have to check if there are any validation errors, currently it's done like this:
public class RootViewModel : BindableBase
{
//class code
if (designInformation.Technology == Technology.CVT)
{
if (designInformation.HasErrors) return;
if (InfoInputViewModel.TrafoProperties.HasErrors) return;
if (InfoInputViewModel.CapacitorVoltageTransformerViewModel.CapacitorVoltageDivider.HasErrors) return;
if (InfoInputViewModel.CapacitorVoltageTransformerViewModel.IntermediateVoltageTransformer.HasErrors) return;
if (SpecialDesignViewModel.SpecialDesignInformation.HasErrors) return;
foreach (var item in InfoInputViewModel.SecondaryWindings.WindingsCollection)
{
if (item.HasErrors) return;
}
performCalculationsCVT();
}
}
And I'm looking for a way to simplify this code by getting all errors from model at once, but don't know where to start with this problem.
Bellow is implementation of INotifyDataErrorInfo interface i use.
public class ValidableBase : BindableBase, INotifyDataErrorInfo
{
protected readonly Dictionary<string, ICollection<string>>
_validationErrors = new Dictionary<string, ICollection<string>>();
#region INotifyDataErrorInfo Implementation
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
protected void RaiseErrorsChanged(string propertyName)
{
if (ErrorsChanged != null)
ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
}
public IEnumerable GetErrors(string propertyName)
{
if (string.IsNullOrEmpty(propertyName) || !_validationErrors.ContainsKey(propertyName))
return null;
return _validationErrors[propertyName];
}
public bool HasErrors
{
get { return _validationErrors.Count > 0; }
}
public void AddError(string propertyName, string message)
{
if (_validationErrors.ContainsKey(propertyName))
{
string value = _validationErrors[propertyName].First();
value += Environment.NewLine;
value += message;
_validationErrors[propertyName] = new List<string> { value };
}
else
_validationErrors[propertyName] = new List<string> { message };
RaiseErrorsChanged(propertyName);
}
public void RemoveError(string propertyName)
{
_validationErrors.Remove(propertyName);
RaiseErrorsChanged(propertyName);
}
[XmlIgnore]
public Dictionary<string, ICollection<string>> ValidationErrors
{
get { return this._validationErrors; }
}
#endregion
}
}
Obviously, the base class has no idea what properties a particular child class has, let alone if they implement INDEI. You'll have to write the logic to do this. There are many ways you can accomplish this.
For me, I'd add an abstract method to the base class thusly
abstract class ValidableBase
{
// snip
protected abstract IEnumerable<ValidableBase> GetValidableProperties();
// snip
Then change HasErrors to call HasErrors recursively on the results of the above call
public bool HasErrors
{
get { return _validationErrors.Count > 0 ||
GetValidableProperties().Any(x => x.HasErrors); }
}
An example implementation of GetValidableProperties might be
protected override IEnumerable<ValidableBase> GetValidableProperties()
{
yield return SomeProperty; // a Validable property
yield return SomeOtherProperty; // this too
foreach(var prop in SomeCollectionProperty) // collection of Validable
yield return prop;
}
Lastly, I'd rename Validable to Validatable, which is the correct (english) spelling. If I were Spanish or French, I'd probably skip that last step.
Related
I am coming up to speed on Xamarin. I am using "Mastering Xamarin.Forms: App architecture techniques for building multi-platform, native mobile apps with Xamarin.Forms 4, 3rd Edition" as a guide. This had me create a custom navigation service.
Here is the implementation (I skipped the interface for brevity)
namespace wfw_dispenser.Services
{
public class XamarinFormsNavService : INavService
{
readonly IDictionary<Type, Type> _map = new Dictionary<Type, Type>();
public event PropertyChangedEventHandler CanGoBackChanged;
public INavigation XamarinFormsNav { get; set; }
public bool CanGoBack => XamarinFormsNav.NavigationStack?.Any() == true;
public async Task GoBack()
{
if (CanGoBack)
{
await XamarinFormsNav.PopAsync(true);
OnCanGoBackChanged();
}
}
public async Task NavigateTo<TVM>()
where TVM : BaseViewModel
{
await NavigateToView(typeof(TVM));
if (XamarinFormsNav.NavigationStack.Last().BindingContext is BaseViewModel)
{
((BaseViewModel)XamarinFormsNav.NavigationStack.Last().BindingContext).Init();
}
}
public async Task NavigateTo<TVM, TParameter>(TParameter parameter)
where TVM : BaseViewModel
{
await NavigateToView(typeof(TVM));
if (XamarinFormsNav.NavigationStack.Last().BindingContext is BaseViewModel<TParameter>)
{
((BaseViewModel<TParameter>)XamarinFormsNav.NavigationStack.Last().BindingContext).Init(parameter);
}
}
public void RemoveLastView()
{
if (XamarinFormsNav.NavigationStack.Count< 2)
{
return;
}
var lastView = XamarinFormsNav.NavigationStack[XamarinFormsNav.NavigationStack.Count - 2];
XamarinFormsNav.RemovePage(lastView);
}
public void ClearBackStack()
{
if (XamarinFormsNav.NavigationStack.Count < 2)
{
return;
}
for (var i = 0; i < XamarinFormsNav.NavigationStack.Count - 1; i++)
{
XamarinFormsNav.RemovePage(XamarinFormsNav.NavigationStack[i]);
}
}
public void NavigateToUri(Uri uri)
{
if (uri == null)
{
throw new ArgumentException("Invalid URI");
}
Device.OpenUri(uri);
}
async Task NavigateToView(Type viewModelType)
{
if (!_map.TryGetValue(viewModelType, out Type viewType))
{
throw new ArgumentException("No view found in view mapping for " + viewModelType.FullName + ".");
}
// Use reflection to get the View's constructor and create an instance of the View
var constructor = viewType.GetTypeInfo()
.DeclaredConstructors
.FirstOrDefault(dc => !dc.GetParameters().Any());
var view = constructor.Invoke(null) as Page;
var vm = ((App)Application.Current).Kernel.GetService(viewModelType);
view.BindingContext = vm;
await XamarinFormsNav.PushAsync(view, true);
}
public void RegisterViewMapping(Type viewModel, Type view)
{
_map.Add(viewModel, view);
}
void OnCanGoBackChanged() => CanGoBackChanged?.Invoke(this, new PropertyChangedEventArgs("CanGoBack"));
}
}
It appears to me that there is a NavigateTo that takes a parameter. I tried it and it kind of goes nowhere without any errors in the log. There's nothing in the text about this method to explain how to use it.
I probably have to do something in the "catching" view model for this. Can someone help me out?
First, you must extend from the parameterized version of BaseViewModel. In your case, since you are passing in a PaymentRequest, this would be:
public class CheckoutViewModel : BaseViewModel<PaymentRequest>
Then BaseViewModel<T> has a virtual Init method that you can implement
public class BaseViewModel<TParameter> : BaseViewModel
{
protected BaseViewModel(INavService navService, IAnalyticsService analyticsService)
: base(navService, analyticsService)
{
}
public override void Init()
{
Init(default(TParameter));
}
public virtual void Init(TParameter parameter)
{
}
}
I'm processing messages from an MSMQ.
For each message there are ten steps such as :
VerifyDates
VerifyPatientInformation
VerifyClinicalInformation
If there is a failure in any one of the three steps, I would like to throw out the message.
Can someone point to a SOLID example demonstrating this principal? I'm not sure how to structure my classes.
For example
public class VerifyDates()
{
private validateSomething ValidateSomething();
public VerifyDates()
{
validateSomething = new ValidateSomething();
}
public bool Verify()
{
validateSomething.IsValid();
}
}
Assuming that VerifyDates() is part of my processing logic, should I be adding a public field to the class bool Flag that returns whether or not to continue processing?
Perhaps I should add an Interface IHasFlag where every class that is used within my processing logic has a Flag that I check at the end to see whether or not to continue message processing?
Processing messages would go something like this
public void ProcessMessages()
{
var verifyDates = new VerifyDates();
if(!verifyDates.IsValid)
{
return;
}
//continue processing
//but every step of the process I would need to check IsValid for that specific class
}
How do you abstract the success/failure of every step within a sequential process?
You could use a fluent API. Something similar to the following:
public interface IFluentValidation<T>
{
bool IsValid { get; }
T ObjectToValidate { get; }
}
internal class FluentValidation<T>: IFluentValidation<T>
{
public bool IsValid { get; }
public T ObjectToValidate { get; }
public FluentValidation(bool isValid, T target)
{
Debug.Assert(target != null);
IsValid = isValid;
ObjectToValidate = target;
}
}
And now build the following extension methods:
public static IFluentValidation<T> ValidateBy<T>(this T target, Predicate<T> validator)
{
if (target == null) throw new ArgumentNullException(name(target));
if (validator == null) throw new ArgumentNullException(name(validator));
if (validator(target))
return new FluentValidation<T>(true, target);
return new FluentValidation<T>(false, target);
}
public static IFluentValidation<T> AndBy<T>(this IFluentValidation<T> target, Predicate<T> validator)
{
if (validator == null) throw new ArgumentNullException(name(validator));
if (!target.IsValid)
return target;
if (validator(target.ObjectToValidate)
return target;
return new FluentValidation<T>(false, target.ObjectToValidate);
}
Implementing an OrBy is trivial as a AndFinallyBy if you want to return the validated object and not a IFluentValidation<T>.
And you would use it like this:
var validated = myObject.ValidateBy(t => //whatever needs to checked t.ObjectToValidate.DateIsValid)
.ThenBy(t => ... //whatever needs to be checked t. ObjectToValidate.PatientIsValid)
.ThenBy(t => ... //t.ObjectToValidate .ClinicalInformationIsValid)
.IsValid;
You can also embellish this a little more and add optional custom error messages in each validation step; public static IFluentValidation<T> AndBy<T>(this IFluentValidation<T> target, Predicate<T> validator, Func<T, string> failureMessageProvider = null) and add the corresponding property string ValidationFailedMessage { get; } to IFluentValidation
I'm using the INotifyDataErrorInfo interface to implement a general MVVM validation mechanism. I'm implementing the interface by calling OnValidate instead of OnPropertyChanged:
public void OnValidate(dynamic value, [CallerMemberName] string propertyName = null)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
Validate(propertyName, value);
}
In the Validate Method I'm generating the validation errors, add them to a Dictionary and raise the ErrorsChanged event if a validation error was found or cleared:
if (entry.Validate(strValue, out errorNumber, out errorString) == false)
{
_validationErrors[propertyName] = new List<string> {errorString};
RaiseErrorsChanged(propertyName);
}
else if (_validationErrors.ContainsKey(propertyName))
{
_validationErrors.Remove(propertyName);
RaiseErrorsChanged(propertyName);
}
The HasErrors property is implemented by looking at the errors dictionary:
public bool HasErrors
{
get { return _validationErrors.Any(kv => kv.Value != null
&& kv.Value.Count > 0); }
}
To prevent the save button from being enabled when there is a validation error - The save command canExecuteMethod looks at the HasErrors property:
private bool IsSaveEnabled()
{
return HasErrors == false;
}
Everything works fine except the case where I'm having binding errors - if the binded value is (for example) an integer a non integer is entered - the textbox's ErrorContent is updated with an error string: "Value 'something' could not be converted".
But the INotifyDataErrorInfo mechanism is not updated about this. The HasErrors remains false and Save is enabled although there is an error in the view.
I would like to find a way to propagate the binding exception to the INotifyDataErrorInfo mechanism so I would be able to:
Disable the Save button (must).
Change the validation error message to a more meaningful error string (nice to have).
I would like to find a general MVVM solution without adding code behind in the view.
Thank you for the help
the string int case doesn't work with MVVM because your viewmodel doesn't get any information because of the binding exception.
I see two ways to get the validation you want:
Just use string properties in your viewmodel and when you have to go to your model just convert the string to your model type.
Create behaviors or "special" controls so the the input in your view is always "convertible" to your viewmodel type.
Btw I use the second approach because I have to :) but the first will always work and seems easier to me.
Here is the solution that I have found. It makes the INotifyDataErrorInfo behave correctly in the ViewModel (When there is any validation error – the HasError is true), and it allows adding validation errors from the viewModel. Other than this, it does not require changes in the view, changes in the binding or converters.
This solution involves:
Adding a custom validation rule.
Adding a base user control (which all view must derive from).
Adding some code in the ViewModel base.
Adding a custom validation rule – Validation Entity which does the actual validation and raises an event when the validation changes:
class ValidationEntity : ValidationRule
{
public string Key { get; set; }
public string BaseName = "Base";
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
var fullPropertyName = BaseName + "." + Key;
ValidationEntry entry;
var validationResult = new ValidationResult(true, null);
if ((entry = ValidationManager.Instance.FindValidation(fullPropertyName)) != null)
{
int errorNumber;
string errorString;
var strValue = (value != null) ? value.ToString() : string.Empty;
if (entry.Validate(strValue, out errorNumber, out errorString) == false)
{
validationResult = new ValidationResult(false, errorString);
}
}
if (OnValidationChanged != null)
{
OnValidationChanged(Key, validationResult);
}
return validationResult;
}
public event Action<string, ValidationResult> OnValidationChanged;
}
Adding a base user control which keeps a list of the active textboxs, and adds the validation rule to each textbox binding:
This is the code at the user control base:
private void OnLoaded(object sender, RoutedEventArgs routedEventArgs)
{
_textBoxes = FindAllTextBoxs(this);
var vm = DataContext as ViewModelBase;
if (vm != null) vm.UpdateAllValidationsEvent += OnUpdateAllValidationsEvent;
foreach (var textbox in _textBoxes)
{
var binding = BindingOperations.GetBinding(textbox, TextBox.TextProperty);
if (binding != null)
{
var property = binding.Path.Path;
var validationEntity = new ValidationEntity {Key = property};
binding.ValidationRules.Add(validationEntity);
validationEntity.ValidationChanged += OnValidationChanged;
}
}
}
private List<TextBox> FindAllTextBoxs(DependencyObject fe)
{
return FindChildren<TextBox>(fe);
}
private List<T> FindChildren<T>(DependencyObject dependencyObject)
where T : DependencyObject
{
var items = new List<T>();
if (dependencyObject is T)
{
items.Add(dependencyObject as T);
return items;
}
var count = VisualTreeHelper.GetChildrenCount(dependencyObject);
for (var i = 0; i < count; i++)
{
var child = VisualTreeHelper.GetChild(dependencyObject, i);
var children = FindChildren<T>(child);
items.AddRange(children);
}
return items;
}
When the ValidationChange event happens – the view is called to be notified about the validation error:
private void OnValidationChanged(string propertyName, ValidationResult validationResult)
{
var vm = DataContext as ViewModelBase;
if (vm != null)
{
if (validationResult.IsValid)
{
vm.ClearValidationErrorFromView(propertyName);
}
else
{
vm.AddValidationErrorFromView(propertyName, validationResult.ErrorContent as string);
}
}
}
The ViewModel base keeps two lists:
_notifyvalidationErrors which is used by the INotifyDataErrorInfo interface to display the validation errors.
_privateValidationErrors which is used to display the errors generated from the Validation rule to the user.
When adding a validation error from the view – the _notifyvalidationErrors is updated with an empty value (just to denote there is a validation error) the error string is not added to the _notifyvalidationErrors. If we add it to there we would get the validation error string twice in the textbox ErrorContent.
The validation error string is also added to _privateValidationErrors (Because we want to be able to keep it at the viewmodel)
This is the code at the ViewModel base:
private readonly Dictionary<string, List<string>> _notifyvalidationErrors =
new Dictionary<string, List<string>>();
private readonly Dictionary<string, List<string>> _privateValidationErrors =
new Dictionary<string, List<string>>();
public void AddValidationErrorFromView(string propertyName, string errorString)
{
_notifyvalidationErrors[propertyName] = new List<string>();
// Add the error to the private dictionary
_privateValidationErrors[propertyName] = new List<string> {errorString};
RaiseErrorsChanged(propertyName);
}
public void ClearValidationErrorFromView(string propertyName)
{
if (_notifyvalidationErrors.ContainsKey(propertyName))
{
_notifyvalidationErrors.Remove(propertyName);
}
if (_privateValidationErrors.ContainsKey(propertyName))
{
_privateValidationErrors.Remove(propertyName);
}
RaiseErrorsChanged(propertyName);
}
The INotifyDataErrorInfo implementation in the view:
public bool HasErrors
{
get { return _notifyvalidationErrors.Any(kv => kv.Value != null); }
}
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public void RaiseErrorsChanged(string propertyName)
{
var handler = ErrorsChanged;
if (handler != null)
handler(this, new DataErrorsChangedEventArgs(propertyName));
}
public IEnumerable GetErrors(string propertyName)
{
List<string> errorsForProperty;
_notifyvalidationErrors.TryGetValue(propertyName, out errorsForProperty);
return errorsForProperty;
}
The user has an option to add validation errors from the view by calling the ViewModelBase AddValidationError and ClearValidationError methods.
public void AddValidationError(string errorString, [CallerMemberName] string propertyName = null)
{
_notifyvalidationErrors[propertyName] = new List<string>{ errorString };
RaiseErrorsChanged(propertyName);
}
public void ClearValidationError([CallerMemberName] string propertyName = null)
{
if (_notifyvalidationErrors.ContainsKey(propertyName))
{
_notifyvalidationErrors.Remove(propertyName);
RaiseErrorsChanged(propertyName);
}
}
The view can get a list of all current validation errors from the ViewModel base by calling the GetValidationErrors and GetValidationErrorsString methods.
public List<string> GetValidationErrors()
{
var errors = new List<string>();
foreach (var key in _notifyvalidationErrors.Keys)
{
errors.AddRange(_notifyvalidationErrors[key]);
if (_privateValidationErrors.ContainsKey(key))
{
errors.AddRange(_privateValidationErrors[key]);
}
}
return errors;
}
public string GetValidationErrorsString()
{
var errors = GetValidationErrors();
var sb = new StringBuilder();
foreach (var error in errors)
{
sb.Append("● ");
sb.AppendLine(error);
}
return sb.ToString();
}
Set
ValidatesOnExceptions="True"
In your Binding expression.
Hi all i am using mvvmcross and portable class libraries , so i cannot use prism or componentmodel data annotations, to validate my classes. basically i have a modelbase that all my models inherit from.
My validate code below is horribly broken, basically im looking for the code that data annotations uses to iterate thru all the properties on my class that is inheriting the base class ,
i have written various attributes that are there own validators inheriting from "validatorBase" which inherits from attribute. i just cannot for the life of me figure out thecode that says ... ok im a class im going to go through all the properties in me that have an attribute of type ValidatorBase and run the validator. my code for these are at the bottom
public class ModelBase
{
private Dictionary<string, IEnumerable<string>> _errors;
public Dictionary<string, IEnumerable<string>> Errors
{
get
{
return _errors;
}
}
protected virtual bool Validate()
{
var propertiesWithChangedErrors = new List<string>();
// Get all the properties decorated with the ValidationAttribute attribute.
var propertiesToValidate = this.GetType().GetRuntimeProperties()
.Where(c => c.GetCustomAttributes(typeof(ValidatorBase)).Any());
foreach (PropertyInfo propertyInfo in propertiesToValidate)
{
var propertyErrors = new List<string>();
TryValidateProperty(propertyInfo, propertyErrors);
// If the errors have changed, save the property name to notify the update at the end of this method.
bool errorsChanged = SetPropertyErrors(propertyInfo.Name, propertyErrors);
if (errorsChanged && !propertiesWithChangedErrors.Contains(propertyInfo.Name))
{
propertiesWithChangedErrors.Add(propertyInfo.Name);
}
}
// Notify each property whose set of errors has changed since the last validation.
foreach (string propertyName in propertiesWithChangedErrors)
{
OnErrorsChanged(propertyName);
OnPropertyChanged(string.Format(CultureInfo.CurrentCulture, "Item[{0}]", propertyName));
}
return _errors.Values.Count == 0;
}
}
here is my validator
public class BooleanRequired : ValidatorBase
{
public override bool Validate(object value)
{
bool retVal = true;
retVal = value != null && (bool)value == true;
var t = this.ErrorMessage;
if (!retVal)
{
ErrorMessage = "Accept is Required";
}
return retVal;
}
}
and here is an example of its usage
[Required(ErrorMessage = "Please enter the Amount")]
public decimal Amount
{
get { return _amount; }
set { _amount = value; }//SetProperty(ref _amount, value); }
}
I have a datagridView, that is bound to a List. This List is made up of my class which contains 2 public properties, a String Name, and another List CustomList. See below:
public class MyClass2
{
public string Name
{ get; set;}
public string Description
{
get;
set;
}
}
public class MyClass
{
List<MyClass2> myList;
public string Name
{
get;
set;
}
public List<MyClass2> CustomList
{
get { return myList ?? (myList= new List<MyClass2>()); }
}
}
And then in my designer page:
List<MyClass> myClassList = new List<MyClass>();
dataGridView.DataSource = myClassList;
As it is right now, the only column that appears in the grid, is the MyClass:Name column, and the CustomList column does not show up. What I'd like is the CustomList column to show and to display something like "Collection" with the "..." button showing, and when it is clicked to have the "Collection Editor" to popup.
Does anyone know if this is possible and how to enable it? If there's a tutorial or anything that would help me out I'd appreciate that too. Thanks.
Using generics, I think, is a clean solution:
public class Sorter<T>: IComparer<T>
{
public string Propiedad { get; set; }
public Sorter(string propiedad)
{
this.Propiedad = propiedad;
}
public int Compare(T x, T y)
{
PropertyInfo property = x.GetType().GetProperty(this.Propiedad);
if (property == null)
throw new ApplicationException("El objeto no tiene la propiedad " + this.Propiedad);
return Comparer.DefaultInvariant.Compare(property.GetValue(x, null), property.GetValue(y, null));
}
}
Usage example:
string orderBy = "propertyName";
bool orderAsc = true;
List<MyExampleClass> myClassList = someMethod();
if (!string.IsNullOrEmpty(orderBy))
{
myClassList.Sort(new Sorter<MyExampleClass>(orderBy));
if (!orderAsc) myClassList.Reverse();
}
Short answer: Yes, you can do it with some code.
Long answer: To write the code is gonna be a pain in the ass, as you would have to know not only how the DataGridView behaves with custom columns, but you would need to know how to expose design time elements at runtime, which requires quite a bit of plumbing. Extensive knowledge about the PropertyGrid must also be known.
Note: This might a fun component to write. (I might actually tackle it if I get some time)
So using the 'button' approach posted by Dave, and some code that I found that implements the CollectionEditor, I can edit the CustomList in MyClass2
Here's my solution, although not quite as clean as I'd like:
Put this class somewhere:
class MyHelper : IWindowsFormsEditorService, IServiceProvider, ITypeDescriptorContext
{
public static void EditValue(IWin32Window owner, object component, string propertyName)
{
PropertyDescriptor prop = TypeDescriptor.GetProperties(component)[propertyName];
if (prop == null) throw new ArgumentException("propertyName");
UITypeEditor editor = (UITypeEditor)prop.GetEditor(typeof(UITypeEditor));
MyHelper ctx = new MyHelper(owner, component, prop);
if (editor != null && editor.GetEditStyle(ctx) == UITypeEditorEditStyle.Modal)
{
object value = prop.GetValue(component);
value = editor.EditValue(ctx, ctx, value);
if (!prop.IsReadOnly)
{
prop.SetValue(component, value);
}
}
}
private readonly IWin32Window owner;
private readonly object component;
private readonly PropertyDescriptor property;
private MyHelper(IWin32Window owner, object component, PropertyDescriptor property)
{
this.owner = owner;
this.component = component;
this.property = property;
}
#region IWindowsFormsEditorService Members
public void CloseDropDown()
{
throw new NotImplementedException();
}
public void DropDownControl(System.Windows.Forms.Control control)
{
throw new NotImplementedException();
}
public System.Windows.Forms.DialogResult ShowDialog(System.Windows.Forms.Form dialog)
{
return dialog.ShowDialog(owner);
}
#endregion
#region IServiceProvider Members
public object GetService(Type serviceType)
{
return serviceType == typeof(IWindowsFormsEditorService) ? this : null;
}
#endregion
#region ITypeDescriptorContext Members
IContainer ITypeDescriptorContext.Container
{
get { return null; }
}
object ITypeDescriptorContext.Instance
{
get { return component; }
}
void ITypeDescriptorContext.OnComponentChanged()
{ }
bool ITypeDescriptorContext.OnComponentChanging()
{
return true;
}
PropertyDescriptor ITypeDescriptorContext.PropertyDescriptor
{
get { return property; }
}
#endregion
Add a button column to the data grid:
DataGridViewButtonColumn butt = new DataGridViewButtonColumn();
butt.HeaderText = "CustomList";
butt.Name = "CustomList";
butt.Text = "Edit CustomList...";
butt.UseColumnTextForButtonValue = true;
dataGridView.Columns.Add(butt);
dataGridView.CellClick += new DataGridViewCellEventHandler(dataGridView_CellClick);
Then call it in the button handler of the cell click.
if (e.RowIndex < 0 || e.ColumnIndex != dataGridView.Columns["CustomList"].Index)
return;
//get the name of this column
string name = (string)dataGridView[dataGridView.Columns["Name"].Index, e.RowIndex].Value;
var myClassObject= myClassList.Find(o => o.Name == name);
MyHelper.EditValue(this, myClassObject, "CustomList");
I'd still be interested in hearing other approaches, and not having to implement my own CollectionEditor. And I'm still interested in having it look more like what the TabControl uses to add TabPages in the PropertyGrid...by displaying the "..." button...but this might work for now.
What you want to do is add a column template with a button in it:
http://geekswithblogs.net/carmelhl/archive/2008/11/11/126942.aspx
In the handler for the button, get the selected MyClass item from the collection and bind its list property to a grid in your popup.