views:

285

answers:

4

I have a viewmodel that implements IDataError. In the viewmodel I have an ObservableCollection. The ObservableCollection populates a datagrid in my view:

    // the list that populates the datagrid
    public ObservableCollection<ProjectExpenseItemsDto> ListOfProjectExpenseItems
    {
        get { return listOfProjectExpenseItems; }
        set
        {
            if (listOfProjectExpenseItems != value)
            {
                listOfProjectExpenseItems = value;
                NotifyPropertyChanged("ListOfProjectExpenseItems");
            }
        }
    }

I also have a property that represents the selected item in the datagrid (it is based off a Dto):

    // the selected row in the datagrid
    public ProjectExpenseItemsDto SelectedProjectExpenseItem
    {
        get { return selectedProjectExpenseItem; }
        set
        {
            if (selectedProjectExpenseItem != value)
            {
                selectedProjectExpenseItem = value;
                NotifyPropertyChanged("SelectedProjectExpenseItem");
            }
        }
    }

Here is the Dto:

namespace ProjectExpense.Model.Dto
{
    [DataContract]
    public class ProjectExpenseItemsDto
    {
        [DataMember]
        public int RowID { get; set; }
        [DataMember]
        public int ProjectExpenseID { get; set; }
        [DataMember]
        public string ItemNumber { get; set; }
        [DataMember]
        public string ItemDescription { get; set; }
        [DataMember]
        public decimal ItemUnitPrice { get; set; }
        [DataMember]
        public decimal ItemQty { get; set; }
        [DataMember]
        public string SupplierName { get; set; }
        [DataMember]
        public DateTime CreateDate { get; set; }
    }
}

I want to use IDataError to validate values in the selected row of the datagrid (SelectedProjectExpenseItem), but since my grid is bound to the ObservableCollection, I don't have any properties in my viewmodel; therefore, I can only use IDataError against the ObservableCollection, not the individual items in the collection, which doesn't help me because I have know way to see "inside" the collection. I cannot use IDataError for my SelectedProjectExpenseItem either. For example:

    string IDataErrorInfo.this[string propertyName]
    {
        get
        {
            string result = string.Empty;
            propertyName = propertyName ?? string.Empty;
            if (propertyName == string.Empty || propertyName == "ItemNumber")
            {
                if (string.IsNullOrEmpty(SelectedProjectExpenseItem.ItemNumber))
                {
                    result = "Name cannot be blank!";
                }
            }
            return result;
        }
    }

this doesn't fire because my datagrid column is not bound to the SelectedProjectExpenseItem.ItemNumber, it is bound to the ItemNumber in the ObservableCollection.

I am looking for any guidance as this is really confusing me.

---------------------------- EDIT: ----------------------------

Ok, I created a separate viewmodel for my DTO:

namespace ProjectExpense.ViewModels
{
    public class ProjectExpenseItemsDtoViewModel : ProjectExpenseItemsDto, IDataErrorInfo
    {
        public ProjectExpenseItemsDtoViewModel()
        {
            Initialize();
        }

        private void Initialize()
        {
        }

        #region Validation

        // string method
        static bool IsStringMissing(string value)
        {
            return String.IsNullOrEmpty(value) || value.Trim() == String.Empty;
        }

        #endregion

        #region IDataErrorInfo Members

        public string Error
        {
            get
            {
                return this[string.Empty];
            }
        }

        public string this[string propertyName]
        {
            get
            {
                string result = string.Empty;
                if (propertyName == "ItemNumber")
                {
                    if (IsStringMissing(this.ItemNumber))
                        result = "Item number cannot be empty!";
                    if (this.ItemNumber.Length > 50)
                        return "Item number exceeds 50 characters";
                }
                return result;
            }
        }

        #endregion
    }
}

Now, I am having problems with the following line in my main vm:

IList<ProjectExpenseItemsDtoViewModel> iList = projectExpenseItemsRepository.GetProjectExpenseItems(ProjectExpenseID);
foreach (ProjectExpenseItemsDtoViewModel item in iList)
   ListOfProjectExpenseItems.Add(item);

It says:

Cannot implicitly convert type 'System.Collections.Generic.IList' to 'System.Collections.Generic.IList'. An explicit conversion exists (are you missing a cast?)

Any ideas?

---------------------------- EDIT: ----------------------------

I found this link, gonna see if I can copy what the person is doing:

Validation-in-a-WPF-DataGrid

A: 

Instead of using an ObservableCollection of ProjectExpenseItemsDto , create a ViewModel for the ProjectExpenseItemsDto Type (I.E. : ProjectExpenseItemsDtoViewModel) and make it implements the IDataErrorInfoInterface, then use an ObservableCollection<ProjectExpenseItemsDtoViewModel> as a property in the main ViewModel.

Islam Ibrahim
Are you saying to create a separate vm for the dto, and use that vm in the main one? Would the dto vm implement any properties, like the field names from the Dto? Hmmm, let me take a look at that. I'll get back after I try it. Thanks.
steveareeno
I hate to be a bother, but could you give me an example of what this vm might look like?
steveareeno
You may need to take a look at this example: http://www.codeproject.com/KB/WPF/WPFDataGridExamples.aspx#errorinfo, its the same idea.
Islam Ibrahim
yeah, I looked at that example but can't seem to make it apply to my project.
steveareeno
A: 

As far as I can tell the problem is not in your viewmodel, but in the markup in your view. You have to implement it on your DTO, you just have to tell your view to pay attention to IDataErrorInfo and do something about it.

you could do the following:

<dg:DataGrid ItemsSource="{StaticResource ListOfProjectExpenseItems}"/> >
<dg:DataGrid.Columns>
    <dg:DataGridTextColumn Header="ItemNumber" Binding="{Binding ItemNumber,ValidatesOnDataErrors=true}"/>
    <dg:DataGridTextColumn Header="ItemDescription" Binding="{Binding ItemDescription" />
</dg:DataGrid.Columns>
</dg:DataGrid>

Note the ValidatedOnDataErrors property. See your viewmodel has to be able to tell the view that something is wrong, so IDataErrorInfo is an interface that WPF inherently listens to, if it's asked. If your binding on the property tells wpf to listen to it then it will.

And there are default error templates, but if lets say you want to add a tooltip to tell the user the actual error you would need to do something like this

<Style x:Key="textBoxInError" TargetType="{x:Type TextBox}">
  <Style.Triggers>
    <Trigger Property="Validation.HasError" Value="true">
      <Setter Property="ToolTip"
        Value="{Binding RelativeSource={x:Static RelativeSource.Self},
                        Path=(Validation.Errors)[0].ErrorContent}"/>
    </Trigger>
  </Style.Triggers>
</Style>

Google wpf error templates to read about error templates like this example. The above will add a tooltip to textboxes that have an error.

WPF has several moving parts, so its easy to get lost, but you'll get used to it, it just takes time :)

Hope this helps you in your journey.

Jose
I did previously have the ValidatesOnDataErrors in the xaml, and I had the styles too. In fact, my ItemQty field (decimal) does work if I type a char or blank it out, and that is with no styles or anything applied to the dg. You are absolutely right about wpf and mvvm. It gets difficult to even post a question because of the number of degrees of separation between your database and view. In my case I have SQL Server DB, EF, Repository, Dto, ViewModel, View.
steveareeno
BTW, thanks for the reply!
steveareeno
A: 

Ok, I found part of the problem. The reason IDataErrorInfo wasn't firing was because I didn't have ValidatesOnDataErrors=True on the SelectedItem binding of the dg:

            <DataGrid ItemsSource="{Binding Path=ListOfProjectExpenseItems, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" AutoGenerateColumns="False" 
                Name="dgProjectExpenseItems" SelectionMode="Single" SelectionUnit="FullRow" CanUserResizeColumns="True" 
                RowStyle="{StaticResource RowStyle}" SelectedItem="{Binding Path=SelectedProjectExpenseItem, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" GridLinesVisibility="Horizontal" CanUserDeleteRows="True" CanUserAddRows="True">
            <DataGrid.RowValidationRules>
                <DataErrorValidationRule ValidationStep="UpdatedValue" />
                <ExceptionValidationRule ValidationStep="UpdatedValue" />
            </DataGrid.RowValidationRules>

            <DataGrid.Columns>
                <DataGridTextColumn Header="ID" Width="SizeToCells"  MinWidth="50" Binding="{Binding RowID}" />
                <DataGridTextColumn Header="Project Expense ID" Width="SizeToCells" Visibility="Hidden" MinWidth="0" Binding="{Binding ProjectExpenseID, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
                <DataGridTextColumn Header="Item Number" EditingElementStyle="{StaticResource CellEditStyle}" Width="SizeToCells" MinWidth="140" Binding="{Binding ItemNumber, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True, ValidatesOnExceptions=True, NotifyOnValidationError=true }" />
                <DataGridTextColumn Header="Item Description" Width="SizeToCells" MinWidth="250" Binding="{Binding ItemDescription, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
                <DataGridTextColumn Header="Unit Price" Width="SizeToCells" MinWidth="90" Binding="{Binding ItemUnitPrice, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
                <DataGridTextColumn Header="Qty" Width="SizeToCells" MinWidth="65" Binding="{Binding ItemQty, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
                <DataGridTextColumn Header="Supplier Name" Width="SizeToCells" MinWidth="200" Binding="{Binding SupplierName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
            </DataGrid.Columns>
        </DataGrid>

My IDataErroIno looks like so:

    string IDataErrorInfo.this[string propertyName]
    {
        get
        {
            string result = string.Empty;
            propertyName = propertyName ?? string.Empty;
            if (propertyName == string.Empty || propertyName == "SelectedProjectExpenseItem")
            {
                if (SelectedProjectExpenseItem != null)
                {
                    if (IsStringMissing(SelectedProjectExpenseItem.ItemNumber))
                    {
                        result = "Item number cannot be blank!";
                        IsValid = false;
                    }
                }
            }
            return result;
        }
    }

Now I just need to figure out how to highlight the cell in error. My defined styles don't seem to be doing the job. I still think it has to do with not having the individual properties in the vm.

wow, this wpf/mvvm stuff can get a guy lost in a hurry.

steveareeno
A: 

The problems had to do with using Dto's in my implementation, or at least the lack of implementing IDataErrorInfo in them. I decided to dump them and go with straight business objects that implement IDataErroInfo and wa-la, everything works beautifully now.

steveareeno