Redaguoti

Bendrinti naudojant


Self-Tracking Entities Walkthrough

Important

We no longer recommend using the self-tracking-entities template. It will only continue to be available to support existing applications. If your application requires working with disconnected graphs of entities, consider other alternatives such as Trackable Entities, which is a technology similar to Self-Tracking-Entities that is more actively developed by the community, or writing custom code using the low-level change tracking APIs.

This walkthrough demonstrates the scenario in which a Windows Communication Foundation (WCF) service exposes an operation that returns an entity graph. Next, a client application manipulates that graph and submits the modifications to a service operation that validates and saves the updates to a database using Entity Framework.

Before completing this walkthrough make sure you read the Self-Tracking Entities page.

This walkthrough completes the following actions:

  • Creates a database to access.
  • Creates a class library that contains the model.
  • Swaps to the Self-Tracking Entity Generator template.
  • Moves the entity classes to a separate project.
  • Creates a WCF service that exposes operations to query and save entities.
  • Creates client applications (Console and WPF) that consume the service.

We'll use Database First in this walkthrough but the same techniques apply equally to Model First.

Pre-Requisites

To complete this walkthrough you will need a recent version of Visual Studio.

Create a Database

The database server that is installed with Visual Studio is different depending on the version of Visual Studio you have installed:

  • If you are using Visual Studio 2012 then you'll be creating a LocalDB database.
  • If you are using Visual Studio 2010 you'll be creating a SQL Express database.

Let's go ahead and generate the database.

  • Open Visual Studio
  • View -> Server Explorer
  • Right click on Data Connections -> Add Connection…
  • If you haven’t connected to a database from Server Explorer before you’ll need to select Microsoft SQL Server as the data source
  • Connect to either LocalDB or SQL Express, depending on which one you have installed
  • Enter STESample as the database name
  • Select OK and you will be asked if you want to create a new database, select Yes
  • The new database will now appear in Server Explorer
  • If you are using Visual Studio 2012
    • Right-click on the database in Server Explorer and select New Query
    • Copy the following SQL into the new query, then right-click on the query and select Execute
  • If you are using Visual Studio 2010
    • Select Data -> Transact SQL Editor -> New Query Connection...
    • Enter .\SQLEXPRESS as the server name and click OK
    • Select the STESample database from the drop down at the top of the query editor
    • Copy the following SQL into the new query, then right-click on the query and select Execute SQL
    CREATE TABLE [dbo].[Blogs] (
        [BlogId] INT IDENTITY (1, 1) NOT NULL,
        [Name] NVARCHAR (200) NULL,
        [Url]  NVARCHAR (200) NULL,
        CONSTRAINT [PK_dbo.Blogs] PRIMARY KEY CLUSTERED ([BlogId] ASC)
    );

    CREATE TABLE [dbo].[Posts] (
        [PostId] INT IDENTITY (1, 1) NOT NULL,
        [Title] NVARCHAR (200) NULL,
        [Content] NTEXT NULL,
        [BlogId] INT NOT NULL,
        CONSTRAINT [PK_dbo.Posts] PRIMARY KEY CLUSTERED ([PostId] ASC),
        CONSTRAINT [FK_dbo.Posts_dbo.Blogs_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [dbo].[Blogs] ([BlogId]) ON DELETE CASCADE
    );

    SET IDENTITY_INSERT [dbo].[Blogs] ON
    INSERT INTO [dbo].[Blogs] ([BlogId], [Name], [Url]) VALUES (1, N'ADO.NET Blog', N'blogs.msdn.com/adonet')
    SET IDENTITY_INSERT [dbo].[Blogs] OFF
    INSERT INTO [dbo].[Posts] ([Title], [Content], [BlogId]) VALUES (N'Intro to EF', N'Interesting stuff...', 1)
    INSERT INTO [dbo].[Posts] ([Title], [Content], [BlogId]) VALUES (N'What is New', N'More interesting stuff...', 1)

Create the Model

First up, we need a project to put the model in.

  • File -> New -> Project...
  • Select Visual C# from the left pane and then Class Library
  • Enter STESample as the name and click OK

Now we'll create a simple model in the EF Designer to access our database:

  • Project -> Add New Item...
  • Select Data from the left pane and then ADO.NET Entity Data Model
  • Enter BloggingModel as the name and click OK
  • Select Generate from database and click Next
  • Enter the connection information for the database that you created in the previous section
  • Enter BloggingContext as the name for the connection string and click Next
  • Check the box next to Tables and click Finish

Swap to STE Code Generation

Now we need to disable the default code generation and swap to Self-Tracking Entities.

If you are using Visual Studio 2012

  • Expand BloggingModel.edmx in Solution Explorer and delete the BloggingModel.tt and BloggingModel.Context.tt This will disable the default code generation
  • Right-click an empty area on the EF Designer surface and select Add Code Generation Item...
  • Select Online from the left pane and search for STE Generator
  • Select the STE Generator for C# template, enter STETemplate as the name and click Add
  • The STETemplate.tt and STETemplate.Context.tt files are added nested under the BloggingModel.edmx file

If you are using Visual Studio 2010

  • Right-click an empty area on the EF Designer surface and select Add Code Generation Item...
  • Select Code from the left pane and then ADO.NET Self-Tracking Entity Generator
  • Enter STETemplate as the name and click Add
  • The STETemplate.tt and STETemplate.Context.tt files are added directly to your project

Move Entity Types into Separate Project

To use Self-Tracking Entities our client application needs access to the entity classes generated from our model. Because we don't want to expose the whole model to the client application we're going to move the entity classes into a separate project.

The first step is to stop generating entity classes in the existing project:

  • Right-click on STETemplate.tt in Solution Explorer and select Properties
  • In the Properties window clear TextTemplatingFileGenerator from the CustomTool property
  • Expand STETemplate.tt in Solution Explorer and delete all files nested under it

Next, we are going to add a new project and generate the entity classes in it

  • File -> Add -> Project...

  • Select Visual C# from the left pane and then Class Library

  • Enter STESample.Entities as the name and click OK

  • Project -> Add Existing Item...

  • Navigate to the STESample project folder

  • Select to view All Files (*.*)

  • Select the STETemplate.tt file

  • Click on the drop down arrow next to the Add button and select Add As Link

    Add Linked Template

We're also going to make sure the entity classes get generated in the same namespace as the context. This just reduces the number of using statements we need to add throughout our application.

  • Right-click on the linked STETemplate.tt in Solution Explorer and select Properties
  • In the Properties window set Custom Tool Namespace to STESample

The code generated by the STE template will need a reference to System.Runtime.Serialization in order to compile. This library is needed for the WCF DataContract and DataMember attributes that are used on the serializable entity types.

  • Right click on the STESample.Entities project in Solution Explorer and select Add Reference...
    • In Visual Studio 2012 - check the box next to System.Runtime.Serialization and click OK
    • In Visual Studio 2010 - select System.Runtime.Serialization and click OK

Finally, the project with our context in it will need a reference to the entity types.

  • Right click on the STESample project in Solution Explorer and select Add Reference...
    • In Visual Studio 2012 - select Solution from the left pane, check the box next to STESample.Entities and click OK
    • In Visual Studio 2010 - select the Projects tab, select STESample.Entities and click OK

Note

Another option for moving the entity types to a separate project is to move the template file, rather than linking it from its default location. If you do this, you will need to update the inputFile variable in the template to provide the relative path to the edmx file (in this example that would be ..\BloggingModel.edmx).

Create a WCF Service

Now it's time to add a WCF Service to expose our data, we'll start by creating the project.

  • File -> Add -> Project...
  • Select Visual C# from the left pane and then WCF Service Application
  • Enter STESample.Service as the name and click OK
  • Add a reference to the System.Data.Entity assembly
  • Add a reference to the STESample and STESample.Entities projects

We need to copy the EF connection string to this project so that it is found at runtime.

  • Open the App.Config file for the **STESample **project and copy the connectionStrings element
  • Paste the connectionStrings element as a child element of the configuration element of the Web.Config file in the STESample.Service project

Now it's time to implement the actual service.

  • Open IService1.cs and replace the contents with the following code
    using System.Collections.Generic;
    using System.ServiceModel;

    namespace STESample.Service
    {
        [ServiceContract]
        public interface IService1
        {
            [OperationContract]
            List<Blog> GetBlogs();

            [OperationContract]
            void UpdateBlog(Blog blog);
        }
    }
  • Open Service1.svc and replace the contents with the following code
    using System;
    using System.Collections.Generic;
    using System.Data;
    using System.Linq;

    namespace STESample.Service
    {
        public class Service1 : IService1
        {
            /// <summary>
            /// Gets all the Blogs and related Posts.
            /// </summary>
            public List<Blog> GetBlogs()
            {
                using (BloggingContext context = new BloggingContext())
                {
                    return context.Blogs.Include("Posts").ToList();
                }
            }

            /// <summary>
            /// Updates Blog and its related Posts.
            /// </summary>
            public void UpdateBlog(Blog blog)
            {
                using (BloggingContext context = new BloggingContext())
                {
                    try
                    {
                        // TODO: Perform validation on the updated order before applying the changes.

                        // The ApplyChanges method examines the change tracking information
                        // contained in the graph of self-tracking entities to infer the set of operations
                        // that need to be performed to reflect the changes in the database.
                        context.Blogs.ApplyChanges(blog);
                        context.SaveChanges();

                    }
                    catch (UpdateException)
                    {
                        // To avoid propagating exception messages that contain sensitive data to the client tier
                        // calls to ApplyChanges and SaveChanges should be wrapped in exception handling code.
                        throw new InvalidOperationException("Failed to update. Try your request again.");
                    }
                }
            }        
        }
    }

Consume the Service from a Console Application

Let's create a console application that uses our service.

  • File -> New -> Project...
  • Select Visual C# from the left pane and then Console Application
  • Enter STESample.ConsoleTest as the name and click OK
  • Add a reference to the STESample.Entities project

We need a service reference to our WCF service

  • Right-click the STESample.ConsoleTest project in Solution Explorer and select Add Service Reference...
  • Click Discover
  • Enter BloggingService as the namespace and click OK

Now we can write some code to consume the service.

  • Open Program.cs and replace the contents with the following code.
    using STESample.ConsoleTest.BloggingService;
    using System;
    using System.Linq;

    namespace STESample.ConsoleTest
    {
        class Program
        {
            static void Main(string[] args)
            {
                // Print out the data before we change anything
                Console.WriteLine("Initial Data:");
                DisplayBlogsAndPosts();

                // Add a new Blog and some Posts
                AddBlogAndPost();
                Console.WriteLine("After Adding:");
                DisplayBlogsAndPosts();

                // Modify the Blog and one of its Posts
                UpdateBlogAndPost();
                Console.WriteLine("After Update:");
                DisplayBlogsAndPosts();

                // Delete the Blog and its Posts
                DeleteBlogAndPost();
                Console.WriteLine("After Delete:");
                DisplayBlogsAndPosts();

                Console.WriteLine("Press any key to exit...");
                Console.ReadKey();
            }

            static void DisplayBlogsAndPosts()
            {
                using (var service = new Service1Client())
                {
                    // Get all Blogs (and Posts) from the service
                    // and print them to the console
                    var blogs = service.GetBlogs();
                    foreach (var blog in blogs)
                    {
                        Console.WriteLine(blog.Name);
                        foreach (var post in blog.Posts)
                        {
                            Console.WriteLine(" - {0}", post.Title);
                        }
                    }
                }

                Console.WriteLine();
                Console.WriteLine();
            }

            static void AddBlogAndPost()
            {
                using (var service = new Service1Client())
                {
                    // Create a new Blog with a couple of Posts
                    var newBlog = new Blog
                    {
                        Name = "The New Blog",
                        Posts =
                        {
                            new Post { Title = "Welcome to the new blog"},
                            new Post { Title = "What's new on the new blog"}
                        }
                    };

                    // Save the changes using the service
                    service.UpdateBlog(newBlog);
                }
            }

            static void UpdateBlogAndPost()
            {
                using (var service = new Service1Client())
                {
                    // Get all the Blogs
                    var blogs = service.GetBlogs();

                    // Use LINQ to Objects to find The New Blog
                    var blog = blogs.First(b => b.Name == "The New Blog");

                    // Update the Blogs name
                    blog.Name = "The Not-So-New Blog";

                    // Update one of the related posts
                    blog.Posts.First().Content = "Some interesting content...";

                    // Save the changes using the service
                    service.UpdateBlog(blog);
                }
            }

            static void DeleteBlogAndPost()
            {
                using (var service = new Service1Client())
                {
                    // Get all the Blogs
                    var blogs = service.GetBlogs();

                    // Use LINQ to Objects to find The Not-So-New Blog
                    var blog = blogs.First(b => b.Name == "The Not-So-New Blog");

                    // Mark all related Posts for deletion
                    // We need to call ToList because each Post will be removed from the
                    // Posts collection when we call MarkAsDeleted
                    foreach (var post in blog.Posts.ToList())
                    {
                        post.MarkAsDeleted();
                    }

                    // Mark the Blog for deletion
                    blog.MarkAsDeleted();

                    // Save the changes using the service
                    service.UpdateBlog(blog);
                }
            }
        }
    }

You can now run the application to see it in action.

  • Right-click the STESample.ConsoleTest project in Solution Explorer and select Debug -> Start new instance

You'll see the following output when the application executes.

Initial Data:
ADO.NET Blog
- Intro to EF
- What is New

After Adding:
ADO.NET Blog
- Intro to EF
- What is New
The New Blog
- Welcome to the new blog
- What's new on the new blog

After Update:
ADO.NET Blog
- Intro to EF
- What is New
The Not-So-New Blog
- Welcome to the new blog
- What's new on the new blog

After Delete:
ADO.NET Blog
- Intro to EF
- What is New

Press any key to exit...

Consume the Service from a WPF Application

Let's create a WPF application that uses our service.

  • File -> New -> Project...
  • Select Visual C# from the left pane and then WPF Application
  • Enter STESample.WPFTest as the name and click OK
  • Add a reference to the STESample.Entities project

We need a service reference to our WCF service

  • Right-click the STESample.WPFTest project in Solution Explorer and select Add Service Reference...
  • Click Discover
  • Enter BloggingService as the namespace and click OK

Now we can write some code to consume the service.

  • Open MainWindow.xaml and replace the contents with the following code.
    <Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:STESample="clr-namespace:STESample;assembly=STESample.Entities"
        mc:Ignorable="d" x:Class="STESample.WPFTest.MainWindow"
        Title="MainWindow" Height="350" Width="525" Loaded="Window_Loaded">

        <Window.Resources>
            <CollectionViewSource
                x:Key="blogViewSource"
                d:DesignSource="{d:DesignInstance {x:Type STESample:Blog}, CreateList=True}"/>
            <CollectionViewSource
                x:Key="blogPostsViewSource"
                Source="{Binding Posts, Source={StaticResource blogViewSource}}"/>
        </Window.Resources>

        <Grid DataContext="{StaticResource blogViewSource}">
            <DataGrid AutoGenerateColumns="False" EnableRowVirtualization="True"
                      ItemsSource="{Binding}" Margin="10,10,10,179">
                <DataGrid.Columns>
                    <DataGridTextColumn Binding="{Binding BlogId}" Header="Id" Width="Auto" IsReadOnly="True" />
                    <DataGridTextColumn Binding="{Binding Name}" Header="Name" Width="Auto"/>
                    <DataGridTextColumn Binding="{Binding Url}" Header="Url" Width="Auto"/>
                </DataGrid.Columns>
            </DataGrid>
            <DataGrid AutoGenerateColumns="False" EnableRowVirtualization="True"
                      ItemsSource="{Binding Source={StaticResource blogPostsViewSource}}" Margin="10,145,10,38">
                <DataGrid.Columns>
                    <DataGridTextColumn Binding="{Binding PostId}" Header="Id" Width="Auto"  IsReadOnly="True"/>
                    <DataGridTextColumn Binding="{Binding Title}" Header="Title" Width="Auto"/>
                    <DataGridTextColumn Binding="{Binding Content}" Header="Content" Width="Auto"/>
                </DataGrid.Columns>
            </DataGrid>
            <Button Width="68" Height="23" HorizontalAlignment="Right" VerticalAlignment="Bottom"
                    Margin="0,0,10,10" Click="buttonSave_Click">Save</Button>
        </Grid>
    </Window>
  • Open the code behind for MainWindow (MainWindow.xaml.cs) and replace the contents with the following code
    using STESample.WPFTest.BloggingService;
    using System.Collections.Generic;
    using System.Linq;
    using System.Windows;
    using System.Windows.Data;

    namespace STESample.WPFTest
    {
        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
            }

            private void Window_Loaded(object sender, RoutedEventArgs e)
            {
                using (var service = new Service1Client())
                {
                    // Find the view source for Blogs and populate it with all Blogs (and related Posts)
                    // from the Service. The default editing functionality of WPF will allow the objects
                    // to be manipulated on the screen.
                    var blogsViewSource = (CollectionViewSource)this.FindResource("blogViewSource");
                    blogsViewSource.Source = service.GetBlogs().ToList();
                }
            }

            private void buttonSave_Click(object sender, RoutedEventArgs e)
            {
                using (var service = new Service1Client())
                {
                    // Get the blogs that are bound to the screen
                    var blogsViewSource = (CollectionViewSource)this.FindResource("blogViewSource");
                    var blogs = (List<Blog>)blogsViewSource.Source;

                    // Save all Blogs and related Posts
                    foreach (var blog in blogs)
                    {
                        service.UpdateBlog(blog);
                    }

                    // Re-query for data to get database-generated keys etc.
                    blogsViewSource.Source = service.GetBlogs().ToList();
                }
            }
        }
    }

You can now run the application to see it in action.

  • Right-click the STESample.WPFTest project in Solution Explorer and select Debug -> Start new instance
  • You can manipulate the data using the screen and save it via the service using the Save button

WPF Main window