none
CAMBIAR EL COLOR DE FILA EN UN DATAGRID DESPUÉS DE EDITAR RRS feed

  • Pregunta

  • Hola, quiero comprobar si el valor en una celda de un datagrid ha variado antes y después de editar. 

    El xaml del datagrid

            <DataGrid x:Name="categoriesDataGrid" RowDetailsVisibilityMode="VisibleWhenSelected" Margin="47,10,36,389" ItemsSource="{Binding Source={StaticResource categoriesViewSource}}" EnableRowVirtualization="True" AutoGenerateColumns="False" CanUserDeleteRows="False" CanUserAddRows="True" SelectedItem="{Binding CategoriaSeleccionada}" BeginningEdit="categoriesDataGrid_BeginningEdit" CellEditEnding="categoriesDataGrid_CellEditEnding">
            <DataGrid.Columns>
                <DataGridTextColumn x:Name="categoryIdColumn" Width="SizeToHeader" Header="Category Id" Binding="{Binding CategoryId, Mode=TwoWay}"  IsReadOnly="True"/>
                <DataGridTextColumn x:Name="nameColumn" Width="SizeToHeader" Header="Name" Binding="{Binding Name, Mode=TwoWay}" />
            </DataGrid.Columns>
          </DataGrid>
    y el código detrás.

    TextBlock antes; TextBlock despues; private void categoriesDataGrid_BeginningEdit(object sender, DataGridBeginningEditEventArgs e) { antes= e.Column.GetCellContent(e.Row) as TextBlock; } private void categoriesDataGrid_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e) { despues = e.Column.GetCellContent(e.Row) as TextBlock;

    bool iguales1= antes.Equals(despues); bool iguales = (antes.Text == despues.Text); }

    iguales1 siempre me devuelve falso, aunque no varíe el contenido, 

    y en iguales me salta la siguiente exceptción "Excepción no controlada del tipo 'System.NullReferenceException'", en la línea "bool iguales=..."

    El objetivo final es comparar el valor antes y después de editar para luego modificar la propiedad del color de fondo de las celdas modificadas. Me gustaría que la comprobación funcionase para cualquier tipo de contenido en la celda, no solo textblok, por lo que antes he intentado haciendo "antes" y "después" de tipo object pero Equals siempre devuelve falso.

    Gracias


    Juan Pablo Toledo España





    • Editado jpabloace domingo, 1 de marzo de 2015 8:49
    martes, 24 de febrero de 2015 21:32

Respuestas

  • Ok, la idea que tenía yo en mente no era como usted la muestra.  Mi idea es que su clase "Categories" (que yo la hubiera llamado Category porque es una única categoría por objeto) implemente INotifyPropertyChanged.

    No uso EF nunca así que no sé si hay forma de pedirle a EF que genere propiedades virtuales.  Si eso fuera posible, el asunto se simplificaría.  Voy a asumir que no es posible, así que voy a crear un ViewModel para la clase Categories.

    namespace XXXX.ViewModels
    {
        public class Categories : NotifyPropertyChangedBase
        {
            private int m_categoryId;
            public int CategoryId
            {
                get { return m_categoryId; }
                set { SaveAndNotify(ref m_categoryId, value); }
            }
            private string m_name;
            public string Name
            {
                get { return m_Name; }
                set { SaveAndNotify(ref m_name, value); }
            }
            //etc.  Cualquier otra propiedad.
        }
    }

    Donde la clase NotifyPropertyChangedBase es algo así:

    public class NotifyPropertyChangedBase : INotifyPropertyChanged
    {
        #region Properties}
        //Esta sería la propiedad "Editado".  La pongo en inglés porque es mi costumbre.
        private bool m_isDirty;
        public bool IsDirty
        {
            get { return m_isDirty; }
            set { SaveAndNotify(ref m_isDirty; value); }
        }
        #endregion
    
        #region INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void RaisePropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler ev = PropertyChanged;
            if (ev != null)
            {
                ev(this, new PropertyChangedEventArgs(propertyName));
            }
        }
        protected virtual bool SaveAndNotify<T>(ref T storage, T newValue, [CallerMemberName] propertyName = null)
        {
            bool changed = !Object.Equals(storage, newValue);
            if (changed)
            {
                storage = newValue;
                RaisePropertyChanged(propertyName);
                if (propertyName != "IsDirty") IsDirty = true;
            }
            return changed;
        }
        #endregion
    }

    Esta implementación de INotifyPropertyChanged no debería darle el problema en que las filas se ponen en rojo solamente por hacer doble clic sobre ellas.  Esto porque el evento PropertyChanged únicamente se genera cuando los valores anterior y actual de las propiedades es realmente diferente.  Ahora vincule la vista a un ViewModel de estos y notará que los colores aparecen cuando el valor de la celda cambia, sin necesidad de guardar cambios.


    Jose R. MCP
    Code Samples

    • Marcado como respuesta jpabloace lunes, 2 de marzo de 2015 21:20
    lunes, 2 de marzo de 2015 14:29

Todas las respuestas

  • Según parece, GetCellContent() le está devolviendo algo que no es de tipo TextBlock en CellEditEnding.  Entonces la variable "despues" es null.

    Pero para lograr su propósito va por el camino incorrecto, diría yo.

    La clase que contiene las propiedades CategoryId y Name debería incluir propiedades llamadas CategoryIdChanged y NameChanged de tipo bool.  Sus valores se calcularían cada vez que la propiedad correspondiente cambia de valor.

    Si su clase implementa INotifyPropertyChanged debería ser bastante sencillo implementar las propiedades adicionales.


    Jose R. MCP
    Code Samples

    viernes, 27 de febrero de 2015 15:52
  • Hola, muchas gracias. Siguiendo su consejo he creado una nueva propiedad y he utilizado un Datatrigger para cambiar el color de la fila, pero no me funciona, solo pone en rojo las celdas después de guardar el contexto (quiero que lo haga antes para tener constancia de qué voy modificando y cuando guarde que vuelvan a estar con el color por defecto) y también pone a rojo las celdas en las que solo he dado doble clic sin modificar nada. Las entidades las creo con EF6, como no quiero modificar el código autogenerado he creado otra clase parcial en la que incluyo la propiedad.

        public partial class Categories ////     Este código se generó a partir de una plantilla con EF6
        {
            public Categories()
            {
                this.Products = new ObservableCollection<Products>();
            }
        
            public int CategoryId { get; set; }
            public string Name { get; set; }   
            public virtual ObservableCollection<Products> Products { get; set; }
        }
    }

    //En otro archivo diferente

    {
        public partial class Categories
        {
            public bool Editado{ get; set; }//Propiedad que se pretende sea true cuando se actualice cualquier otra propiedad
        }
    }

    En el view Model

    public class MW2ViewModel : INotifyPropertyChanged { private ProductContext _context = new ProductContext(); public MW2ViewModel() { this.Categorias = new ObservableCollection<Categories>(_context.Categories.ToList<Categories>()); .........

    } private ObservableCollection<Categories> _categorias; public ObservableCollection<Categories> Categorias { get { return _categorias; } set { _categorias = value; NotifyPropertyChanged(); } } private Categories _categoriaSeleccionada; public Categories CategoriaSeleccionada { get { return _categoriaSeleccionada; } set { _categoriaSeleccionada = value; //Categories prueba= Categorias.Where(x=>x.CategoryId==CategoriaSeleccionada.CategoryId) ProductosSeleccionados = _categoriaSeleccionada.Products; _categoriaSeleccionada.Editado = true; NotifyPropertyChanged(); } } private void NotifyPropertyChanged([CallerMemberName] String propertyName = "") { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } }

    y el XAML de la vista

    <Window.DataContext>
            <local:MW2ViewModel/>
    </Window.DataContext>
        <Window.Resources>
            <CollectionViewSource x:Key="categoriesViewSource" Source="{Binding Categorias}"/>   
    ....
            <DataGrid x:Name="categoriesDataGrid" RowDetailsVisibilityMode="VisibleWhenSelected" Margin="51.5,10,31.5,389" ItemsSource="{Binding Source={StaticResource categoriesViewSource}}" EnableRowVirtualization="True" AutoGenerateColumns="False" CanUserDeleteRows="False" CanUserAddRows="True" SelectedItem="{Binding CategoriaSeleccionada}">
                <DataGrid.RowStyle>
                    <Style TargetType="DataGridRow">
                        <Style.Triggers>
                            <DataTrigger Binding="{Binding Editado}" Value="true">
                                <Setter Property="Background" Value="red">                                
                                </Setter>
                            </DataTrigger>
                        </Style.Triggers>
                    </Style>
                </DataGrid.RowStyle>
            <DataGrid.Columns>            
                <DataGridTextColumn x:Name="categoryIdColumn" Width="SizeToHeader" Header="Category Id" Binding="{Binding CategoryId, Mode=TwoWay}"  IsReadOnly="True"/>
                <DataGridTextColumn x:Name="nameColumn" Width="SizeToHeader" Header="Name" Binding="{Binding Name, Mode=TwoWay}" />
                    </DataGrid.Columns>
          </DataGrid>

    EDITADO.

    En el botón guardar tengo un Trigger que actualiza el datagrid y por eso muestra el cambio de color en las celdas al guardar.

                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="Click">
                        <ei:CallMethodAction TargetObject="{Binding Items, ElementName=categoriesDataGrid}" MethodName="Refresh"/
                    </i:EventTrigger>
                </i:Interaction.Triggers>

    ¿Pero no debería hacerse solo,el cambio de color de celda, al estar vinculado a una propiedad que implementa INotifyPropertyChanged?

    ¿Por qué se muestra también como si hubiesen cambiado las celdas en las que solo he hecho clic, sin tan siquiera editarlas?.

    Muchas Gracias




    • Editado jpabloace lunes, 2 de marzo de 2015 7:17
    domingo, 1 de marzo de 2015 7:13
  • Ok, la idea que tenía yo en mente no era como usted la muestra.  Mi idea es que su clase "Categories" (que yo la hubiera llamado Category porque es una única categoría por objeto) implemente INotifyPropertyChanged.

    No uso EF nunca así que no sé si hay forma de pedirle a EF que genere propiedades virtuales.  Si eso fuera posible, el asunto se simplificaría.  Voy a asumir que no es posible, así que voy a crear un ViewModel para la clase Categories.

    namespace XXXX.ViewModels
    {
        public class Categories : NotifyPropertyChangedBase
        {
            private int m_categoryId;
            public int CategoryId
            {
                get { return m_categoryId; }
                set { SaveAndNotify(ref m_categoryId, value); }
            }
            private string m_name;
            public string Name
            {
                get { return m_Name; }
                set { SaveAndNotify(ref m_name, value); }
            }
            //etc.  Cualquier otra propiedad.
        }
    }

    Donde la clase NotifyPropertyChangedBase es algo así:

    public class NotifyPropertyChangedBase : INotifyPropertyChanged
    {
        #region Properties}
        //Esta sería la propiedad "Editado".  La pongo en inglés porque es mi costumbre.
        private bool m_isDirty;
        public bool IsDirty
        {
            get { return m_isDirty; }
            set { SaveAndNotify(ref m_isDirty; value); }
        }
        #endregion
    
        #region INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void RaisePropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler ev = PropertyChanged;
            if (ev != null)
            {
                ev(this, new PropertyChangedEventArgs(propertyName));
            }
        }
        protected virtual bool SaveAndNotify<T>(ref T storage, T newValue, [CallerMemberName] propertyName = null)
        {
            bool changed = !Object.Equals(storage, newValue);
            if (changed)
            {
                storage = newValue;
                RaisePropertyChanged(propertyName);
                if (propertyName != "IsDirty") IsDirty = true;
            }
            return changed;
        }
        #endregion
    }

    Esta implementación de INotifyPropertyChanged no debería darle el problema en que las filas se ponen en rojo solamente por hacer doble clic sobre ellas.  Esto porque el evento PropertyChanged únicamente se genera cuando los valores anterior y actual de las propiedades es realmente diferente.  Ahora vincule la vista a un ViewModel de estos y notará que los colores aparecen cuando el valor de la celda cambia, sin necesidad de guardar cambios.


    Jose R. MCP
    Code Samples

    • Marcado como respuesta jpabloace lunes, 2 de marzo de 2015 21:20
    lunes, 2 de marzo de 2015 14:29
  • Muchas gracias. Con el código anterior se soluciona el problema de que se modifiquen solo con hacer doble clic, pero no se actualiza el datagrid automáticamente al ejecutar el SaveAndNofify.

    Si agrego en el formulario un botón con el objetivo de refrescar el datagrid, si me aparecen dichas filas con el color cambiado. ¿Esto no debería hacerse solo al cambiar el contexto?.

    código xaml implicado

        <Window.Resources>
            <CollectionViewSource x:Key="categoriesViewSource" Source="{Binding Categorias, Mode=TwoWay}"/>
            <CollectionViewSource x:Key="categoriesProductsViewSource" Source="{Binding Products, Source={StaticResource categoriesViewSource}, Mode=TwoWay}"/>
        </Window.Resources>
        <Window.Triggers>
            <EventTrigger RoutedEvent="ButtonBase.Click" SourceName="button"/>
        </Window.Triggers>
    ....
            <DataGrid x:Name="categoriesDataGrid" RowDetailsVisibilityMode="VisibleWhenSelected" Margin="51.5,10,31.5,389" ItemsSource="{Binding Source={StaticResource categoriesViewSource}}" EnableRowVirtualization="True" AutoGenerateColumns="False" CanUserDeleteRows="False" CanUserAddRows="True" SelectedItem="{Binding CategoriaSeleccionada}" >
    
                <DataGrid.Resources>
                    <Style TargetType="{x:Type DataGridRow}">
                        <Style.Triggers>
                            <DataTrigger Binding="{Binding Editado}" Value="true">
                                <Setter Property="Background" Value="Green"/>
                            </DataTrigger>
                        </Style.Triggers>
                    </Style>
                </DataGrid.Resources>
                <DataGrid.Columns>
                    <DataGridTextColumn x:Name="categoryIdColumn" Width="SizeToHeader" Header="Category Id" Binding="{Binding CategoryId, Mode=TwoWay}"  IsReadOnly="True"/>
                    <DataGridTextColumn x:Name="nameColumn" Width="SizeToHeader" Header="Name" Binding="{Binding Name, Mode=TwoWay}" />
                </DataGrid.Columns>
            </DataGrid>
            ....
            <Button Content="Refrescar" Grid.Column="1" HorizontalAlignment="Left" Margin="38,126,0,0" VerticalAlignment="Top" Width="75">
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="Click">
                        <ei:CallMethodAction TargetObject="{Binding Items, ElementName=categoriesDataGrid}" MethodName="Refresh"/>
                        <ei:CallMethodAction TargetObject="{Binding Items, ElementName=producsDataGrid}" MethodName="Refresh"/>
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </Button>
    


    Juan Pablo Toledo España

    lunes, 2 de marzo de 2015 19:25
  • Pues nunca he tenido problemas con esa implementación que le mostré de INotifyPropertyChanged.  Veo que el ItemsSource de ese DataGrid es el objeto estático "categoriesViewSource", que a su vez es la colección en la propiedad "Categorias" del objeto que es el DataContext del padre del DataGrid (¿ventana?).  Supongo que ese objeto es su ViewModel para esta vista.  ¿Estoy en lo correcto?  ¿Esa colección contiene objetos tipo ViewModel.Categories que usan el INotifyPropertyChanged que le mostré?

    Jose R. MCP
    Code Samples

    lunes, 2 de marzo de 2015 19:35
  • Hola, gracias. 

    El DataGrid, como se ve arriba tiene Binding a "Categorias", pero no entra en el "set" de dicha propiedad al modificarla, solo al insertar otra fila (en cuyo caso si cambia de color). Al editar entra en el "set" de "CategoriaSeleccionada", pero no en el de Categorias, _categorías varia no obstante, pero no actualiza la vista. He probado a forzar un "Raise property changed" con Categorias dentro del set de CategoriaSeleccionada, pero tampoco.


    Juan Pablo Toledo España

    lunes, 2 de marzo de 2015 20:31
  • Ok, si su aplicación trabaja reemplazando objetos, esta solución no le funcionará.  Esta solución asume que la grilla opera sobre los objetos que originalmente le fueron dados.  Si usted reemplaza un objeto por otro que representa el mismo dato pero con cambios, la propiedad IsDirty vendrá siempre en False.

    Ahora bien, yo probé esta solución y me funciona según lo esperado con objetos "ejemplo" creados en memoria.  Entonces le pregunto:  ¿Crea usted objetos de reemplazo en vez de actualizar propiedades del objeto existente?


    Jose R. MCP
    Code Samples

    lunes, 2 de marzo de 2015 20:35
  • Hola, gracias, creo que tiene razón, el problema del mapeo que hago del DBContext (ProductContext) que genera el ADO.NET Entity DataModel hacia Categorias.

    Nunca pasa por el "set" de dicha propiedad (que es el binding del data grid) por lo que nunca actualiza el contexto, aunque lo está cambiando por otro lado en el contexto (_context) porque al guardar dichos cambios si se reflejan en la base de datos.

    El objeto al que está vinculado el Data grid en modo twoway es Categorias, este es el código

    public class MainWindowVM : INotifyPropertyChanged { private ProductContext _context = new ProductContext(); public MainWindowVM() { _context.Categories.ToList();

    _saveCommand = new RelayCommand(o => _context.SaveChanges(), o => _context.ChangeTracker.HasChanges()); } public ObservableCollection<Category> _categorias; public ObservableCollection<Category> Categorias { get { return _context.Categories.Local; } set //Problema-->Por aquí nunca pasa { SaveAndNotify(ref _categorias, value); } }

    Pero nunca pasa por "set", con lo que no se entera que el contexto cambia, pero si lo guardo este cambio se refleja en la base de datos.  



    Juan Pablo Toledo España




    • Editado jpabloace lunes, 2 de marzo de 2015 22:50
    lunes, 2 de marzo de 2015 22:45
  • Bueno, no me queda claro si su problema se resolvió o no según lo que contesta.  Marca la respuesta, pero no parece convencido.

    El asunto está así:  Objetivo:  Cuando un dato cambie en la grilla, la fila debe cambiar de color indicando que el registro fue cambiado.  Cuando esto sucede, el registro solamente ha cambiado en memoria; el registro no ha cambiado en base de datos aún.

    ¿Correcto?

    Para lograr eso cómodamente en WPF MVVM, cada objeto que representa un item en la grilla debe implementar INotifyPropertyChanged, y cada vez que una propiedad cambia, la propiedad IsDirty debe cambiar a True para que el estilo de la fila aplique.  Eso yo lo comprobé y funciona correctamente.

    La diferencia entre usted y yo:  Yo no uso EF nunca.  Me estorba.  Usted sí lo usa y por lo tanto tiene una clase POCO Categories que no le es de ninguna utilidad para lograr este objetivo.  Mi propuesta:  Cree una nueva clase Categories que sí implemente INotifyPropertyChanged.  Cuando el botón Guardar sea presionado, pase los valores de las propiedades de esta nueva clase a la clase POCO que genera EF para que pueda grabar a base de datos.

    Usted me muestra una implementación de VM llamada MainWindowVM que contiene una propiedad llamada Categorias.  Esa propiedad no interesa.  O sea, interesa que exista para que provea los datos a la grilla, pero lo realmente importante es que los elementos contenidos en esa colección implementen INotifyPropertyChanged tal como lo describí arriba.  Esa sería la nueva clase Categories de la que yo hablo.  Es irrelevante si el set de la propiedad MainWindowVM.Categorias corre o no; lo único que es relevante es que el evento PropertyChanged se genere cuando se cambien datos en los elementos de la colección mencionada porque eso será el mecanismo por el cual la propiedad IsDirty adquiere el valor True.

    ¿Queda más claro?  ¿Logré explicarme mejor?


    Jose R. MCP
    Code Samples

    martes, 3 de marzo de 2015 4:19
  • Hola, gracias. He marcado esa respuesta porque es parte de la solución (detectar el cambio no solo la edición).

    El supuesto anterior, lo he probado a hacer, modificando directamente la clase POCO Categories que genera automáticamente. SI funciona, al editar actualiza el color.

    ¿Hay alguna forma de poder complementar la entidad Categories sin necesidad tocar la clase generada automáticamente?. He intentado implementar el INotifyPropertyChanged para por ejemplo la propiedad Name en una clase parcial, pero sin éxito. ¿Con metadatos se podría hacer?

    Me reitero en mis agradecimientos por seguir prestando atención al tema.


    Juan Pablo Toledo España





    • Editado jpabloace martes, 3 de marzo de 2015 20:49
    martes, 3 de marzo de 2015 18:52
  • Pues no sé si será posible obtener notificaciones de cambio de propiedades de la clase POCO.  La única forma que se me ocurre es que data binding del DataGrid use PropertyDescriptor's para leer y escribir valores de propiedades, tal y como lo hace el DataGridView de Windows Forms.  Si eso fuera cierto, entonces en un archivo aparte podría implementarse ICustomTypeDescriptor para proveer PropertyDescriptor's personalizados que aparte de hacer su trabajo, se encargarían de la ejecución de INotifyPropertyChanged.

    Pero como le digo, no sé si el mecanismo del DataGrid en WPF usa o no PropertyDescriptor's.


    Jose R. MCP
    Code Samples

    martes, 3 de marzo de 2015 21:46
  • Hola. Gracias. Voy a probar con un paquete Nuget llamado PropertyChanged.Foody que según leo es tan sencillo de usar como el ejemplo adjunta y ya implementa INPC en la clase.

    using PropertyChanged;
    
    [ImplementPropertyChanged]
    public partial class Customer
    {
    }
    Aquí hay más información


    Juan Pablo Toledo España



    • Editado jpabloace martes, 3 de marzo de 2015 22:30
    martes, 3 de marzo de 2015 22:21