Crear una aplicación de datos sencilla con WPF y Entity Framework 6

Advertencia

Si usa Visual Studio 2022, debe usar Visual Studio 2022, versión 17.3 Preview 3 o una posterior para este tutorial.

En este tutorial se muestra cómo crear una aplicación básica de "formularios sobre datos" en Visual Studio. La aplicación usa SQL Server LocalDB, la base de datos Northwind, Entity Framework 6 (no Entity Framework Core) y Windows Presentation Foundation para .NET Framework (no .NET Core ni .NET 5 o una versión posterior). Muestra cómo realizar el enlace de datos básico con una vista de maestro y detalles, y también tiene un navegador de enlaces personalizado con botones para Mover siguiente, Mover anterior, Mover al principio, Mover al final, Actualizar y Eliminar.

Este artículo se centra en el uso de herramientas de datos en Visual Studio y no intenta explicar en profundidad las tecnologías subyacentes. Se da por hecho que conoce XAML, Entity Framework y SQL a un nivel básico. En este ejemplo tampoco se muestra la arquitectura Modelo-Vista-Modelo de vista (MVVM), que es estándar para las aplicaciones WPF. Pero puede copiar este código en su propia aplicación MVVM con pocas modificaciones.

El código final de este tutorial está en GitHub en Ejemplos de tutorial de Visual Studio (EF6).

Instalación y conexión a Northwind

En este ejemplo se usa LocalDB de SQL Server Express y la base de datos de ejemplo Northwind. Si el proveedor de datos ADO.NET para ese producto admite Entity Framework, también debe funcionar con otros productos de base de datos SQL.

  1. Si no tiene LocalDB de SQL Server Express, instálelo mediante el Instalador de Visual Studio. En el Instalador de Visual Studio, puede instalar LocalDB de SQL Server Express como parte de la carga de trabajo Almacenamiento y procesamiento de datos, o bien como un componente individual.

  2. Siga estos pasos para instalar la base de datos de ejemplo Northwind:

    1. En Visual Studio, abra la ventana Explorador de objetos de SQL Server (Explorador de objetos de SQL Server se instala como parte de la carga de trabajo Almacenamiento y procesamiento de datos en el Instalador de Visual Studio). Expanda el nodo SQL Server. Haga clic con el botón derecho en la instancia de LocalDB y seleccione Nueva consulta.

      Se abre una ventana del editor de consultas.

    2. Copie el script de Transact-SQL Northwind en el Portapapeles. Este script de T-SQL crea la base de datos Northwind desde cero y la rellena con datos.

    3. Pegue el script de T-SQL en el editor de consultas y, después, elija el botón Ejecutar.

      Después de un breve tiempo, la consulta termina de ejecutarse y se crea la base de datos Northwind.

  3. Agregue nuevas conexiones para Northwind.

Configuración del proyecto

  1. En Visual Studio, cree un proyecto de aplicación WPF (.NET Framework) de C#.

  2. Agregue el paquete NuGet para Entity Framework 6. En el Explorador de soluciones, seleccione el nodo del proyecto. En el menú principal, elija Proyecto>Manage NuGet Packages (Administrar paquetes NuGet).

  3. En el Administrador de paquetes NuGet, haga clic en el vínculo Examinar. Entity Framework probablemente sea el paquete principal de la lista. Haga clic en Instalar en el panel derecho y siga las indicaciones. La ventana de salida indica cuándo finaliza la instalación.

    Captura de pantalla del paquete NuGet de Entity Framework.

    Captura de pantalla en la que se muestra el paquete NuGet de Entity Framework.

  4. Ahora puede usar Visual Studio para crear un modelo basado en la base de datos Northwind.

Creación del modelo

  1. Haga clic con el botón derecho en el nodo del proyecto en el Explorador de soluciones y seleccione Agregar>Nuevo elemento. En el panel izquierdo, en el nodo C#, elija Datos y, en el panel central, Entity Data Model de ADO.NET.

    Captura de pantalla del nuevo elemento del modelo de Entity Framework.

    Captura de pantalla del nuevo elemento del modelo de Entity Framework.

  2. Llame al modelo Northwind_model y elija Agregar. Se abre el Asistente para Entity Data Model. Elija EF Designer desde base de datos y, después, haga clic en Siguiente.

    Captura de pantalla del modelo de EF desde la base de datos.

  3. En la pantalla siguiente, elija la conexión LocalDB Northwind (por ejemplo, (localdb)\MSSQLLocalDB), especifique la base de datos Northwind y haga clic en Siguiente.

    Si no ve una conexión, elija Nueva conexión y, después, en el cuadro de diálogo Elegir origen de datos, elija Microsoft SQL Server, Continuar y, en el cuadro de diálogo Propiedades de la conexión, escriba (localdb)\MSSQLLocalDB y , en Select or enter a database name (Seleccionar o escribir un nombre de base de datos), elija Northwind y, después, presione Aceptar.

  4. Si se le solicita, elija la versión de Entity Framework que usa.

    Captura de pantalla en la que se muestran las opciones de la versión.

  5. En la página siguiente del asistente, elija qué tablas, procedimientos almacenados y otros objetos de base de datos se incluirán en el modelo de Entity Framework. Expanda el nodo dbo en la vista de árbol y elija Customers, Orders y Order Details. Deje activados los valores predeterminados y haga clic en Finalizar.

    Captura de pantalla de la selección de los objetos de base de la datos para el modelo.

  6. El asistente genera las clases de C# que representan el modelo de Entity Framework. Estas son clases de C# antiguas sin formato y son las que se enlazaron con datos a la interfaz de usuario de WPF. El archivo .edmx describe las relaciones y otros metadatos que asocian las clases a objetos de la base de datos. Los archivos .tt son plantillas T4 que generan el código que funciona en el modelo y guardan los cambios en la base de datos. Puede ver todos estos archivos en el Explorador de soluciones, en el nodo Northwind_model:

    Captura de pantalla en la que se muestran los archivos del modelo de Entity Framework del Explorador de soluciones.

    Captura de pantalla en la que se muestran los archivos del modelo de Entity Framework del Explorador de soluciones

    La superficie del diseñador para el archivo .edmx permite modificar algunas propiedades y relaciones en el modelo. En este tutorial no vamos a usar el diseñador.

  7. Los archivos .tt son de uso general y debe ajustar uno de ellos para que funcione con el enlace de datos de WPF, que requiere ObservableCollections. En el Explorador de soluciones, expanda el nodo Northwind_model hasta que encuentre Northwind_model.tt. Asegúrese de que no está en el archivo .Context.tt, que está justo debajo del archivo .edmx.

  8. Presione F5 o Ctrl+F5 para compilar y ejecutar el proyecto. Cuando se ejecuta la aplicación por primera vez, las clases del modelo son visibles en el Asistente para orígenes de datos.

Ahora ya está a punto para enlazar este modelo a la página XAML a fin de poder ver los datos, navegar por ellos y modificarlos.

Enlace de datos del modelo a la página XAML

Es posible escribir su propio código de enlace de datos, pero es mucho más fácil dejar que Visual Studio lo haga automáticamente.

  1. En el menú principal, elija Proyecto>Agregar nuevo origen de datos para abrir el Asistente para la configuración del origen de datos. Elija Objeto porque está enlazando a las clases del modelo, no a la base de datos:

    Captura de pantalla del Asistente para la configuración de orígenes de datos con el origen Objeto.

  2. Expanda el nodo del proyecto y seleccione Customer. (Los orígenes de Orders se generan automáticamente a partir de la propiedad de navegación Orders en Customer).

    Captura de pantalla en la que se muestra cómo agregar clases de entidad como orígenes de datos.

    Captura de pantalla en la que se muestra cómo agregar clases de entidad como orígenes de datos.

  3. Haga clic en Finalizar

  4. Vaya a MainWindow.xaml en la vista Código. Para este ejemplo estamos empleando un lenguaje XAML sencillo. Cambie el título de MainWindow a algo más descriptivo, y aumente su alto y ancho a 600 x 800 por el momento. Siempre puede cambiarlo más adelante. Ahora agregue estas tres definiciones de fila a la cuadrícula principal, una fila para los botones de navegación, una para los detalles del cliente y otra para la cuadrícula en la que se muestran sus pedidos:

        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
    
  5. Abra MainWindow.xaml ahora para poder verlo en el diseñador. Esto hace que la ventana Orígenes de datos aparezca como una opción en el margen de la ventana de Visual Studio, junto al Cuadro de herramientas. Haga clic en la pestaña para abrir la ventana, presione Mayús+Alt+D o elija Ver>Otras ventanas>Orígenes de datos. Vamos a mostrar cada propiedad de la clase Customers en su propio cuadro de texto individual. En primer lugar, haga clic en la flecha del cuadro combinado Customers y elija Detalles. Después, arrastre el nodo a la parte central de la superficie de diseño para que el diseñador sepa que quiere situarlo en la fila central. Si lo coloca incorrectamente, más adelante podrá especificar la fila manualmente en el XAML (Grid.Row="1"). De manera predeterminada, los controles se colocan verticalmente en un elemento de cuadrícula, pero en este momento, puede organizarlos como prefiera en el formulario. Por ejemplo, podría tener sentido colocar el cuadro de texto Nombre en la parte superior, encima de la dirección. La aplicación de ejemplo de este artículo reordena los campos y los reorganiza en dos columnas.

    Captura de pantalla en la que se muestra el enlace de origen de datos Customers a controles individuales.

    Captura de pantalla en la que se muestra el enlace de origen de datos Customers a controles individuales.

    En la vista XAML, ahora puede ver un elemento Grid nuevo en la fila 1 (fila central) de la cuadrícula primaria. La cuadrícula primaria tiene un atributo DataContext que hace referencia a un elemento CollectionViewSource que se ha agregado al elemento Windows.Resources. Dado ese contexto de datos, cuando el primer cuadro de texto se enlaza a Address, ese nombre se asigna a la propiedad Address del objeto Customer en CollectionViewSource.

    <Grid DataContext="{StaticResource customerViewSource}">
    
  6. Cuando un cliente está visible en la mitad superior de la ventana, quiere que sus pedidos aparezcan en la mitad inferior. Los pedidos se muestran en un único control de vista de cuadrícula. Para que el enlace de datos de maestro y detalles funcione según lo previsto, es importante que se enlace a la propiedad Orders de la clase Customers, no al nodo Orders independiente. Arrastre la propiedad Orders de la clase Customers a la mitad inferior del formulario, de modo que el diseñador la coloque en la fila 2:

    Captura de pantalla en la que se muestran las clases Orders arrastradas y colocadas como una cuadrícula.

    Captura de pantalla en la que se muestran las clases Orders arrastradas y colocadas como una cuadrícula.

  7. Visual Studio ha generado todo el código de enlace que conecta los controles de la interfaz de usuario a eventos del modelo. Todo lo que necesita hacer para ver algunos datos es escribir código a fin de rellenar el modelo. En primer lugar, vaya a MainWindow.xaml.cs y agregue un miembro de datos a la clase MainWindow para el contexto de datos. Este objeto, que se ha generado automáticamente, actúa como un control que realiza un seguimiento de los cambios y eventos del modelo. También agregará miembros de datos CollectionViewSource para clientes y pedidos, y la lógica de inicialización del constructor asociado al constructor MainWindow() existente. La parte superior de la clase debe ser como esta:

    public partial class MainWindow : Window
    {
        NorthwindEntities context = new NorthwindEntities();
        CollectionViewSource custViewSource;
        CollectionViewSource ordViewSource;
    
        public MainWindow()
        {
            InitializeComponent();
            custViewSource = ((CollectionViewSource)(FindResource("customerViewSource")));
            ordViewSource = ((CollectionViewSource)(FindResource("customerOrdersViewSource")));
            DataContext = this;
        }
    

    Si aún no existe, agregue una directiva using para System.Data.Entity a fin de que el método de extensión Load entre en el ámbito:

    using System.Data.Entity;
    

    Ahora, desplácese hacia abajo y busque el controlador de eventos Window_Loaded. Observe que Visual Studio ha agregado un objeto CollectionViewSource. Esto representa el objeto NorthwindEntities que seleccionó al crear el modelo. Eso ya lo ha agregado, por lo que aquí no lo necesita. Vamos a reemplazar el código en Window_Loaded para que el método tenga el aspecto siguiente:

    private void Window_Loaded(object sender, RoutedEventArgs e)
    {
        // Load is an extension method on IQueryable,    
        // defined in the System.Data.Entity namespace.   
        // This method enumerates the results of the query,    
        // similar to ToList but without creating a list.   
        // When used with Linq to Entities, this method    
        // creates entity objects and adds them to the context.   
        context.Customers.Load();
    
        // After the data is loaded, call the DbSet<T>.Local property    
        // to use the DbSet<T> as a binding source.   
        custViewSource.Source = context.Customers.Local;
    }
    
  8. Presione F5. Debería ver los detalles del primer cliente que se recuperó en CollectionViewSource. También debería ver sus pedidos en la cuadrícula de datos. Como el formato no es excelente, vamos a corregirlo. También puede crear una manera de ver los demás registros y realizar operaciones CRUD básicas de creación, lectura, actualización y eliminación.

Ajuste del diseño de la página e incorporación de cuadrículas para nuevos clientes y pedidos

La disposición predeterminada que genera Visual Studio no es ideal para la aplicación, por lo que proporcionaremos el lenguaje XAML final aquí para copiarlo en el código. También necesita algunos "formularios" (que en realidad son Cuadrículas) para permitir que el usuario agregue un cliente o pedido nuevo. Para poder agregar un cliente y un pedido nuevos, necesita un conjunto independiente de cuadros de texto que no están enlazados a datos a CollectionViewSource. Controlará la cuadrícula que ve el usuario en cualquier momento estableciendo la propiedad Visible en los métodos de controlador. Por último, agregue un botón Eliminar a cada fila de la cuadrícula Pedidos para permitir que el usuario elimine un pedido individual.

En primer lugar, agregue estos estilos al elemento Windows.Resources en MainWindow.xaml:

<Style x:Key="Label" TargetType="{x:Type Label}" BasedOn="{x:Null}">
    <Setter Property="HorizontalAlignment" Value="Left"/>
    <Setter Property="VerticalAlignment" Value="Center"/>
    <Setter Property="Margin" Value="3"/>
    <Setter Property="Height" Value="23"/>
</Style>
<Style x:Key="CustTextBox" TargetType="{x:Type TextBox}" BasedOn="{x:Null}">
    <Setter Property="HorizontalAlignment" Value="Right"/>
    <Setter Property="VerticalAlignment" Value="Center"/>
    <Setter Property="Margin" Value="3"/>
    <Setter Property="Height" Value="26"/>
    <Setter Property="Width" Value="120"/>
</Style>

A continuación, reemplace toda la cuadrícula externa por este marcado:

<Grid>
     <Grid.RowDefinitions>
         <RowDefinition Height="auto"/>
         <RowDefinition Height="auto"/>
         <RowDefinition Height="*"/>
     </Grid.RowDefinitions>
     <Grid x:Name="existingCustomerGrid" Grid.Row="1" HorizontalAlignment="Left" Margin="5" Visibility="Visible" VerticalAlignment="Top" Background="AntiqueWhite" DataContext="{StaticResource customerViewSource}">
         <Grid.ColumnDefinitions>
             <ColumnDefinition Width="Auto" MinWidth="233"/>
             <ColumnDefinition Width="Auto" MinWidth="397"/>
         </Grid.ColumnDefinitions>
         <Grid.RowDefinitions>
             <RowDefinition Height="Auto"/>
             <RowDefinition Height="Auto"/>
             <RowDefinition Height="Auto"/>
             <RowDefinition Height="Auto"/>
             <RowDefinition Height="Auto"/>
             <RowDefinition Height="Auto"/>
         </Grid.RowDefinitions>
         <Label Content="Customer ID:" Grid.Row="0" Style="{StaticResource Label}"/>
         <TextBox x:Name="customerIDTextBox" Grid.Row="0" Style="{StaticResource CustTextBox}"
                  Text="{Binding CustomerID, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true}"/>
         <Label Content="Company Name:" Grid.Row="1" Style="{StaticResource Label}"/>
         <TextBox x:Name="companyNameTextBox" Grid.Row="1" Style="{StaticResource CustTextBox}"
                  Text="{Binding CompanyName, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true}"/>
         <Label Content="Contact Name:" Grid.Row="2" Style="{StaticResource Label}"/>
         <TextBox x:Name="contactNameTextBox" Grid.Row="2" Style="{StaticResource CustTextBox}"
                  Text="{Binding ContactName, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true}"/>
         <Label Content="Contact title:" Grid.Row="3" Style="{StaticResource Label}"/>
         <TextBox x:Name="contactTitleTextBox" Grid.Row="3" Style="{StaticResource CustTextBox}"
                  Text="{Binding ContactTitle, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true}"/>
         <Label Content="Address:" Grid.Row="4" Style="{StaticResource Label}"/>
         <TextBox x:Name="addressTextBox" Grid.Row="4" Style="{StaticResource CustTextBox}"
                  Text="{Binding Address, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true}"/>
         <Label Content="City:" Grid.Column="1" Grid.Row="0" Style="{StaticResource Label}"/>
         <TextBox x:Name="cityTextBox" Grid.Column="1" Grid.Row="0" Style="{StaticResource CustTextBox}"
                  Text="{Binding City, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true}"/>
         <Label Content="Country:" Grid.Column="1" Grid.Row="1" Style="{StaticResource Label}"/>
         <TextBox x:Name="countryTextBox" Grid.Column="1" Grid.Row="1" Style="{StaticResource CustTextBox}"
                  Text="{Binding Country, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true}"/>
         <Label Content="Fax:" Grid.Column="1" Grid.Row="2" Style="{StaticResource Label}"/>
         <TextBox x:Name="faxTextBox" Grid.Column="1" Grid.Row="2" Style="{StaticResource CustTextBox}"
                  Text="{Binding Fax, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true}"/>
         <Label Content="Phone:" Grid.Column="1" Grid.Row="3" Style="{StaticResource Label}"/>
         <TextBox x:Name="phoneTextBox" Grid.Column="1" Grid.Row="3" Style="{StaticResource CustTextBox}"
                  Text="{Binding Phone, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true}"/>
         <Label Content="Postal Code:" Grid.Column="1" Grid.Row="4" VerticalAlignment="Center" Style="{StaticResource Label}"/>
         <TextBox x:Name="postalCodeTextBox" Grid.Column="1" Grid.Row="4" Style="{StaticResource CustTextBox}"
                  Text="{Binding PostalCode, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true}"/>
         <Label Content="Region:" Grid.Column="1" Grid.Row="5" Style="{StaticResource Label}"/>
         <TextBox x:Name="regionTextBox" Grid.Column="1" Grid.Row="5" Style="{StaticResource CustTextBox}"
                  Text="{Binding Region, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true}"/>
     </Grid>
     <Grid x:Name="newCustomerGrid" Grid.Row="1" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="5" DataContext="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=newCustomer, UpdateSourceTrigger=Explicit}" Visibility="Collapsed" Background="CornflowerBlue">
         <Grid.ColumnDefinitions>
             <ColumnDefinition Width="Auto" MinWidth="233"/>
             <ColumnDefinition Width="Auto" MinWidth="397"/>
         </Grid.ColumnDefinitions>
         <Grid.RowDefinitions>
             <RowDefinition Height="Auto"/>
             <RowDefinition Height="Auto"/>
             <RowDefinition Height="Auto"/>
             <RowDefinition Height="Auto"/>
             <RowDefinition Height="Auto"/>
             <RowDefinition Height="Auto"/>
         </Grid.RowDefinitions>
         <Label Content="Customer ID:" Grid.Row="0" Style="{StaticResource Label}"/>
         <TextBox x:Name="add_customerIDTextBox" Grid.Row="0" Style="{StaticResource CustTextBox}"
                  Text="{Binding CustomerID, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true}"/>
         <Label Content="Company Name:" Grid.Row="1" Style="{StaticResource Label}"/>
         <TextBox x:Name="add_companyNameTextBox" Grid.Row="1" Style="{StaticResource CustTextBox}"
                  Text="{Binding CompanyName, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true }"/>
         <Label Content="Contact Name:" Grid.Row="2" Style="{StaticResource Label}"/>
         <TextBox x:Name="add_contactNameTextBox" Grid.Row="2" Style="{StaticResource CustTextBox}"
                  Text="{Binding ContactName, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true}"/>
         <Label Content="Contact title:" Grid.Row="3" Style="{StaticResource Label}"/>
         <TextBox x:Name="add_contactTitleTextBox" Grid.Row="3" Style="{StaticResource CustTextBox}"
                  Text="{Binding ContactTitle, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true}"/>
         <Label Content="Address:" Grid.Row="4" Style="{StaticResource Label}"/>
         <TextBox x:Name="add_addressTextBox" Grid.Row="4" Style="{StaticResource CustTextBox}"
                  Text="{Binding Address, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true}"/>
         <Label Content="City:" Grid.Column="1" Grid.Row="0" Style="{StaticResource Label}"/>
         <TextBox x:Name="add_cityTextBox" Grid.Column="1" Grid.Row="0" Style="{StaticResource CustTextBox}"
                  Text="{Binding City, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true}"/>
         <Label Content="Country:" Grid.Column="1" Grid.Row="1" Style="{StaticResource Label}"/>
         <TextBox x:Name="add_countryTextBox" Grid.Column="1" Grid.Row="1" Style="{StaticResource CustTextBox}"
                  Text="{Binding Country, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true}"/>
         <Label Content="Fax:" Grid.Column="1" Grid.Row="2" Style="{StaticResource Label}"/>
         <TextBox x:Name="add_faxTextBox" Grid.Column="1" Grid.Row="2" Style="{StaticResource CustTextBox}"
                  Text="{Binding Fax, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true}"/>
         <Label Content="Phone:" Grid.Column="1" Grid.Row="3" Style="{StaticResource Label}"/>
         <TextBox x:Name="add_phoneTextBox" Grid.Column="1" Grid.Row="3" Style="{StaticResource CustTextBox}"
                  Text="{Binding Phone, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true}"/>
         <Label Content="Postal Code:" Grid.Column="1" Grid.Row="4" VerticalAlignment="Center" Style="{StaticResource Label}"/>
         <TextBox x:Name="add_postalCodeTextBox" Grid.Column="1" Grid.Row="4" Style="{StaticResource CustTextBox}"
                  Text="{Binding PostalCode, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true}"/>
         <Label Content="Region:" Grid.Column="1" Grid.Row="5" Style="{StaticResource Label}"/>
         <TextBox x:Name="add_regionTextBox" Grid.Column="1" Grid.Row="5" Style="{StaticResource CustTextBox}"
                  Text="{Binding Region, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true}"/>
     </Grid>
     <Grid x:Name="newOrderGrid" Grid.Row="1" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="5" DataContext="{Binding Path=newOrder, Mode=TwoWay}" Visibility="Collapsed" Background="LightGreen">
         <Grid.ColumnDefinitions>
             <ColumnDefinition Width="Auto" MinWidth="233"/>
             <ColumnDefinition Width="Auto" MinWidth="397"/>
         </Grid.ColumnDefinitions>
         <Grid.RowDefinitions>
             <RowDefinition Height="Auto"/>
             <RowDefinition Height="Auto"/>
             <RowDefinition Height="Auto"/>
             <RowDefinition Height="Auto"/>
             <RowDefinition Height="Auto"/>
             <RowDefinition Height="Auto"/>
             <RowDefinition Height="Auto"/>
         </Grid.RowDefinitions>
         <Label Content="New Order Form" FontWeight="Bold"/>
         <Label Content="Employee ID:"  Grid.Row="1" Style="{StaticResource Label}"/>
         <TextBox x:Name="add_employeeIDTextBox" Grid.Row="1" Style="{StaticResource CustTextBox}"
                  Text="{Binding EmployeeID, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true}"/>
         <Label Content="Order Date:"  Grid.Row="2" Style="{StaticResource Label}"/>
         <DatePicker x:Name="add_orderDatePicker" Grid.Row="2"  HorizontalAlignment="Right" Width="120"
                 SelectedDate="{Binding OrderDate, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true, UpdateSourceTrigger=PropertyChanged}"/>
         <Label Content="Required Date:" Grid.Row="3" Style="{StaticResource Label}"/>
         <DatePicker x:Name="add_requiredDatePicker" Grid.Row="3" HorizontalAlignment="Right" Width="120"
                  SelectedDate="{Binding RequiredDate, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true, UpdateSourceTrigger=PropertyChanged}"/>
         <Label Content="Shipped Date:"  Grid.Row="4"  Style="{StaticResource Label}"/>
         <DatePicker x:Name="add_shippedDatePicker"  Grid.Row="4"  HorizontalAlignment="Right" Width="120"
                 SelectedDate="{Binding ShippedDate, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true, UpdateSourceTrigger=PropertyChanged}"/>
         <Label Content="Ship Via:"  Grid.Row="5" Style="{StaticResource Label}"/>
         <TextBox x:Name="add_ShipViaTextBox"  Grid.Row="5" Style="{StaticResource CustTextBox}"
                  Text="{Binding ShipVia, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true}"/>
         <Label Content="Freight"  Grid.Row="6" Style="{StaticResource Label}"/>
         <TextBox x:Name="add_freightTextBox" Grid.Row="6" Style="{StaticResource CustTextBox}"
                  Text="{Binding Freight, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true}"/>
     </Grid>
     <DataGrid x:Name="ordersDataGrid" SelectionUnit="Cell" SelectionMode="Single" AutoGenerateColumns="False" CanUserAddRows="false" IsEnabled="True" EnableRowVirtualization="True" Width="auto" ItemsSource="{Binding Source={StaticResource customerOrdersViewSource}}" Margin="10,10,10,10" Grid.Row="2" RowDetailsVisibilityMode="VisibleWhenSelected">
         <DataGrid.Columns>
             <DataGridTemplateColumn>
                 <DataGridTemplateColumn.CellTemplate>
                     <DataTemplate>
                         <Button Content="Delete" Command="{StaticResource DeleteOrderCommand}" CommandParameter="{Binding}"/>
                     </DataTemplate>
                 </DataGridTemplateColumn.CellTemplate>
             </DataGridTemplateColumn>
             <DataGridTextColumn x:Name="customerIDColumn" Binding="{Binding CustomerID}" Header="Customer ID" Width="SizeToHeader"/>
             <DataGridTextColumn x:Name="employeeIDColumn" Binding="{Binding EmployeeID}" Header="Employee ID" Width="SizeToHeader"/>
             <DataGridTextColumn x:Name="freightColumn" Binding="{Binding Freight}" Header="Freight" Width="SizeToHeader"/>
             <DataGridTemplateColumn x:Name="orderDateColumn" Header="Order Date" Width="SizeToHeader">
                 <DataGridTemplateColumn.CellTemplate>
                     <DataTemplate>
                         <DatePicker SelectedDate="{Binding OrderDate, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true, UpdateSourceTrigger=PropertyChanged}"/>
                     </DataTemplate>
                 </DataGridTemplateColumn.CellTemplate>
             </DataGridTemplateColumn>
             <DataGridTextColumn x:Name="orderIDColumn" Binding="{Binding OrderID}" Header="Order ID" Width="SizeToHeader"/>
             <DataGridTemplateColumn x:Name="requiredDateColumn" Header="Required Date" Width="SizeToHeader">
                 <DataGridTemplateColumn.CellTemplate>
                     <DataTemplate>
                         <DatePicker SelectedDate="{Binding RequiredDate, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true, UpdateSourceTrigger=PropertyChanged}"/>
                     </DataTemplate>
                 </DataGridTemplateColumn.CellTemplate>
             </DataGridTemplateColumn>
             <DataGridTextColumn x:Name="shipAddressColumn" Binding="{Binding ShipAddress}" Header="Ship Address" Width="SizeToHeader"/>
             <DataGridTextColumn x:Name="shipCityColumn" Binding="{Binding ShipCity}" Header="Ship City" Width="SizeToHeader"/>
             <DataGridTextColumn x:Name="shipCountryColumn" Binding="{Binding ShipCountry}" Header="Ship Country" Width="SizeToHeader"/>
             <DataGridTextColumn x:Name="shipNameColumn" Binding="{Binding ShipName}" Header="Ship Name" Width="SizeToHeader"/>
             <DataGridTemplateColumn x:Name="shippedDateColumn" Header="Shipped Date" Width="SizeToHeader">
                 <DataGridTemplateColumn.CellTemplate>
                     <DataTemplate>
                         <DatePicker SelectedDate="{Binding ShippedDate, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true, UpdateSourceTrigger=PropertyChanged}"/>
                     </DataTemplate>
                 </DataGridTemplateColumn.CellTemplate>
             </DataGridTemplateColumn>
             <DataGridTextColumn x:Name="shipPostalCodeColumn" Binding="{Binding ShipPostalCode}" Header="Ship Postal Code" Width="SizeToHeader"/>
             <DataGridTextColumn x:Name="shipRegionColumn" Binding="{Binding ShipRegion}" Header="Ship Region" Width="SizeToHeader"/>
             <DataGridTextColumn x:Name="shipViaColumn" Binding="{Binding ShipVia}" Header="Ship Via" Width="SizeToHeader"/>
         </DataGrid.Columns>
     </DataGrid>
 </Grid>

Incorporación de botones para navegar, agregar, actualizar y eliminar

En las aplicaciones de Windows Forms, obtendrá un objeto BindingNavigator con botones para navegar por las filas de una base de datos y realizar operaciones CRUD básicas. WPF no proporciona un objeto BindingNavigator, pero crear uno es relativamente sencillo. Se hace con botones dentro de un objeto StackPanel horizontal y se asocian los botones a los comandos que están enlazados a métodos del código subyacente.

Hay cuatro partes en la lógica de comandos: (1) los comandos, (2) los enlaces, (3) los botones y (4) los controladores de comandos en el código subyacente.

Incorporación de comandos, enlaces y botones en XAML

  1. En primer lugar, agregue los comandos en el archivo MainWindow.xaml dentro del elemento Windows.Resources:

    <RoutedUICommand x:Key="FirstCommand" Text="First"/>
    <RoutedUICommand x:Key="LastCommand" Text="Last"/>
    <RoutedUICommand x:Key="NextCommand" Text="Next"/>
    <RoutedUICommand x:Key="PreviousCommand" Text="Previous"/>
    <RoutedUICommand x:Key="DeleteCustomerCommand" Text="Delete Customer"/>
    <RoutedUICommand x:Key="DeleteOrderCommand" Text="Delete Order"/>
    <RoutedUICommand x:Key="UpdateCommand" Text="Update"/>
    <RoutedUICommand x:Key="AddCommand" Text="Add"/>
    <RoutedUICommand x:Key="CancelCommand" Text="Cancel"/>
    
  2. CommandBinding asigna un evento RoutedUICommand a un método en el código subyacente. Agregue este elemento CommandBindings después de la etiqueta de cierre Windows.Resources:

    <Window.CommandBindings>
        <CommandBinding Command="{StaticResource FirstCommand}" Executed="FirstCommandHandler"/>
        <CommandBinding Command="{StaticResource LastCommand}" Executed="LastCommandHandler"/>
        <CommandBinding Command="{StaticResource NextCommand}" Executed="NextCommandHandler"/>
        <CommandBinding Command="{StaticResource PreviousCommand}" Executed="PreviousCommandHandler"/>
        <CommandBinding Command="{StaticResource DeleteCustomerCommand}" Executed="DeleteCustomerCommandHandler"/>
        <CommandBinding Command="{StaticResource DeleteOrderCommand}" Executed="DeleteOrderCommandHandler"/>
        <CommandBinding Command="{StaticResource UpdateCommand}" Executed="UpdateCommandHandler"/>
        <CommandBinding Command="{StaticResource AddCommand}" Executed="AddCommandHandler"/>
        <CommandBinding Command="{StaticResource CancelCommand}" Executed="CancelCommandHandler"/>
    </Window.CommandBindings>
    
  3. Ahora, agregue StackPanel con los botones de navegación, agregar, eliminar y actualizar. En primer lugar, agregue este estilo a Windows.Resources:

    <Style x:Key="NavButton" TargetType="{x:Type Button}" BasedOn="{x:Null}">
        <Setter Property="FontSize" Value="24"/>
        <Setter Property="FontFamily" Value="Segoe UI Symbol"/>
        <Setter Property="Margin" Value="2,2,2,0"/>
        <Setter Property="Width" Value="40"/>
        <Setter Property="Height" Value="auto"/>
    </Style>
    

    En segundo lugar, pegue este código justo después de RowDefinitions para el elemento Grid externo, hacia la parte superior de la página XAML:

    <StackPanel Orientation="Horizontal" Margin="2,2,2,0" Height="36" VerticalAlignment="Top" Background="Gainsboro" DataContext="{StaticResource customerViewSource}" d:LayoutOverrides="LeftMargin, RightMargin, TopMargin, BottomMargin">
        <Button Name="btnFirst" Content="|◄" Command="{StaticResource FirstCommand}" Style="{StaticResource NavButton}"/>
        <Button Name="btnPrev" Content="◄" Command="{StaticResource PreviousCommand}" Style="{StaticResource NavButton}"/>
        <Button Name="btnNext" Content="►" Command="{StaticResource NextCommand}" Style="{StaticResource NavButton}"/>
        <Button Name="btnLast" Content="►|" Command="{StaticResource LastCommand}" Style="{StaticResource NavButton}"/>
        <Button Name="btnDelete" Content="Delete Customer" Command="{StaticResource DeleteCustomerCommand}" FontSize="11" Width="120" Style="{StaticResource NavButton}"/>
        <Button Name="btnAdd" Content="New Customer" Command="{StaticResource AddCommand}" FontSize="11" Width="80" Style="{StaticResource NavButton}"/>
        <Button Content="New Order" Name="btnNewOrder" FontSize="11" Width="80" Style="{StaticResource NavButton}" Click="NewOrder_click"/>
        <Button Name="btnUpdate" Content="Commit" Command="{StaticResource UpdateCommand}" FontSize="11" Width="80" Style="{StaticResource NavButton}"/>
        <Button Content="Cancel" Name="btnCancel" Command="{StaticResource CancelCommand}" FontSize="11" Width="80" Style="{StaticResource NavButton}"/>
    </StackPanel>
    

Incorporación de controladores de comandos a la clase MainWindow

El código subyacente es mínimo, excepto para los métodos add y delete. La navegación se realiza mediante una llamada a métodos en la propiedad View de CollectionViewSource. DeleteOrderCommandHandler muestra cómo realizar una eliminación en cascada en un pedido. Primero tenemos que eliminar los objetos Order_Details que están asociados a él. UpdateCommandHandler agrega un nuevo cliente o pedido a la colección, o simplemente actualiza un cliente o pedido existente con los cambios que ha realizado el usuario en los cuadros de texto.

Agregue estos métodos de controlador a la clase MainWindow en MainWindow.xaml.cs. Si la colección CollectionViewSource para la tabla Customers tiene un nombre diferente, debe ajustar el nombre en cada uno de estos métodos:

private void LastCommandHandler(object sender, ExecutedRoutedEventArgs e)
{
    custViewSource.View.MoveCurrentToLast();
}

private void PreviousCommandHandler(object sender, ExecutedRoutedEventArgs e)
{
    custViewSource.View.MoveCurrentToPrevious();
}

private void NextCommandHandler(object sender, ExecutedRoutedEventArgs e)
{
    custViewSource.View.MoveCurrentToNext();
}

private void FirstCommandHandler(object sender, ExecutedRoutedEventArgs e)
{
    custViewSource.View.MoveCurrentToFirst();
}

private void DeleteCustomerCommandHandler(object sender, ExecutedRoutedEventArgs e)
{
    // If existing window is visible, delete the customer and all their orders.  
    // In a real application, you should add warnings and allow the user to cancel the operation.  
    var cur = custViewSource.View.CurrentItem as Customer;

    var cust = (from c in context.Customers
                where c.CustomerID == cur.CustomerID
                select c).FirstOrDefault();

    if (cust != null)
    {
        foreach (var ord in cust.Orders.ToList())
        {
            Delete_Order(ord);
        }
        context.Customers.Remove(cust);
    }
    context.SaveChanges();
    custViewSource.View.Refresh();
}

// Commit changes from the new customer form, the new order form,  
// or edits made to the existing customer form.  
private void UpdateCommandHandler(object sender, ExecutedRoutedEventArgs e)
{
    if (newCustomerGrid.IsVisible)
    {
        // Create a new object because the old one  
        // is being tracked by EF now.  
        Customer newCustomer = new Customer
        {
            Address = add_addressTextBox.Text,
            City = add_cityTextBox.Text,
            CompanyName = add_companyNameTextBox.Text,
            ContactName = add_contactNameTextBox.Text,
            ContactTitle = add_contactTitleTextBox.Text,
            Country = add_countryTextBox.Text,
            CustomerID = add_customerIDTextBox.Text,
            Fax = add_faxTextBox.Text,
            Phone = add_phoneTextBox.Text,
            PostalCode = add_postalCodeTextBox.Text,
            Region = add_regionTextBox.Text
        };

        // Perform very basic validation  
        if (newCustomer.CustomerID.Length == 5)
        {
            // Insert the new customer at correct position:  
            int len = context.Customers.Local.Count();
            int pos = len;
            for (int i = 0; i < len; ++i)
            {
                if (String.CompareOrdinal(newCustomer.CustomerID, context.Customers.Local[i].CustomerID) < 0)
                {
                    pos = i;
                    break;
                }
            }
            context.Customers.Local.Insert(pos, newCustomer);
            custViewSource.View.Refresh();
            custViewSource.View.MoveCurrentTo(newCustomer);
        }
        else
        {
            MessageBox.Show("CustomerID must have 5 characters.");
        }

        newCustomerGrid.Visibility = Visibility.Collapsed;
        existingCustomerGrid.Visibility = Visibility.Visible;
    }
    else if (newOrderGrid.IsVisible)
    {
        // Order ID is auto-generated so we don't set it here.  
        // For CustomerID, address, etc we use the values from current customer.  
        // User can modify these in the datagrid after the order is entered.  

        Customer currentCustomer = (Customer)custViewSource.View.CurrentItem;

        Order newOrder = new Order()
        {
            OrderDate = add_orderDatePicker.SelectedDate,
            RequiredDate = add_requiredDatePicker.SelectedDate,
            ShippedDate = add_shippedDatePicker.SelectedDate,
            CustomerID = currentCustomer.CustomerID,
            ShipAddress = currentCustomer.Address,
            ShipCity = currentCustomer.City,
            ShipCountry = currentCustomer.Country,
            ShipName = currentCustomer.CompanyName,
            ShipPostalCode = currentCustomer.PostalCode,
            ShipRegion = currentCustomer.Region
        };

        try
        {
            newOrder.EmployeeID = Int32.Parse(add_employeeIDTextBox.Text);
        }
        catch
        {
            MessageBox.Show("EmployeeID must be a valid integer value.");
            return;
        }

        try
        {
            // Exercise for the reader if you are using Northwind:  
            // Add the Northwind Shippers table to the model.
            
            // Acceptable ShipperID values are 1, 2, or 3.  
            if (add_ShipViaTextBox.Text == "1" || add_ShipViaTextBox.Text == "2"
                || add_ShipViaTextBox.Text == "3")
            {
                newOrder.ShipVia = Convert.ToInt32(add_ShipViaTextBox.Text);
            }
            else
            {
                MessageBox.Show("Shipper ID must be 1, 2, or 3 in Northwind.");
                return;
            }
        }
        catch
        {
            MessageBox.Show("Ship Via must be convertible to int");
            return;
        }

        try
        {
            newOrder.Freight = Convert.ToDecimal(add_freightTextBox.Text);
        }
        catch
        {
            MessageBox.Show("Freight must be convertible to decimal.");
            return;
        }

        // Add the order into the EF model  
        context.Orders.Add(newOrder);
        ordViewSource.View.Refresh();
    }

    // Save the changes, either for a new customer, a new order  
    // or an edit to an existing customer or order.
    context.SaveChanges();
}

// Sets up the form so that user can enter data. Data is later  
// saved when user clicks Commit.  
private void AddCommandHandler(object sender, ExecutedRoutedEventArgs e)
{
    existingCustomerGrid.Visibility = Visibility.Collapsed;
    newOrderGrid.Visibility = Visibility.Collapsed;
    newCustomerGrid.Visibility = Visibility.Visible;

    // Clear all the text boxes before adding a new customer.  
    foreach (var child in newCustomerGrid.Children)
    {
        var tb = child as TextBox;
        if (tb != null)
        {
            tb.Text = "";
        }
    }
}

private void NewOrder_click(object sender, RoutedEventArgs e)
{
    var cust = custViewSource.View.CurrentItem as Customer;
    if (cust == null)
    {
        MessageBox.Show("No customer selected.");
        return;
    }

    existingCustomerGrid.Visibility = Visibility.Collapsed;
    newCustomerGrid.Visibility = Visibility.Collapsed;
    newOrderGrid.UpdateLayout();
    newOrderGrid.Visibility = Visibility.Visible;
}

// Cancels any input into the new customer form  
private void CancelCommandHandler(object sender, ExecutedRoutedEventArgs e)
{
    add_addressTextBox.Text = "";
    add_cityTextBox.Text = "";
    add_companyNameTextBox.Text = "";
    add_contactNameTextBox.Text = "";
    add_contactTitleTextBox.Text = "";
    add_countryTextBox.Text = "";
    add_customerIDTextBox.Text = "";
    add_faxTextBox.Text = "";
    add_phoneTextBox.Text = "";
    add_postalCodeTextBox.Text = "";
    add_regionTextBox.Text = "";

    existingCustomerGrid.Visibility = Visibility.Visible;
    newCustomerGrid.Visibility = Visibility.Collapsed;
    newOrderGrid.Visibility = Visibility.Collapsed;
}

private void Delete_Order(Order order)
{
    // Find the order in the EF model.  
    var ord = (from o in context.Orders.Local
               where o.OrderID == order.OrderID
               select o).FirstOrDefault();

    // Delete all the order_details that have  
    // this Order as a foreign key  
    foreach (var detail in ord.Order_Details.ToList())
    {
        context.Order_Details.Remove(detail);
    }

    // Now it's safe to delete the order.  
    context.Orders.Remove(ord);
    context.SaveChanges();

    // Update the data grid.  
    ordViewSource.View.Refresh();
}

private void DeleteOrderCommandHandler(object sender, ExecutedRoutedEventArgs e)
{
    // Get the Order in the row in which the Delete button was clicked.  
    Order obj = e.Parameter as Order;
    Delete_Order(obj);
}

Ejecución de la aplicación

Presione F5 para iniciar la depuración. Debería ver los datos de clientes y pedidos rellenados en la cuadrícula y los botones de navegación deberían funcionar según lo previsto. Haga clic en Confirmar para agregar un nuevo cliente o pedido al modelo después de haber introducido los datos. Haga clic en Cancelar para salir de un formulario nuevo de pedido o cliente sin guardar los datos. Puede realizar ediciones en los clientes y pedidos existentes directamente en los cuadros de texto; esos cambios se escribirán automáticamente en el modelo.