-
With the SharePoint 2010 Beta came out of the door, one of the exciting news is that it allows developers to access SharePoint List Data using ADO.Net Data Service 1.5. If you are familiar with Data Service Client programming mode (we have already posted some articles here), it will be very easy to integrate SharePoint List Data in your application.
Once you set up the SharePoint with ADO.Net Data Service (see How-To here ), you can check the Data Service through http://<SharePointServer>/_vti_bin/ListData.svc. Now we are going to show you how to program against the service in Visual Studio 2010 Beta2.
In Visual Studio 2010, developers can add Data Source to ADO.Net Data Service through Data Source Configuration Wizard, you can do the same thing for SharePoint Data service. Here we create a Visual Basic WPF Application, after launching the Wizard (Menu->Data->Add New Data Source), we can see a SharePoint entry as following.

After selecting the SharePoint Icon and clicking “Next” button, “Add Service Reference” will be launched. Instead of remembering the long address of the service, you only need to input the SharePoint site (eg, my sample site is http://vspro-vm-bdc1 ) then click “Go” button. Visual Studio will heuristically find the service address for you. Once it’s populated, you can click “OK” button and finish adding the data source.


Then you can open the Data Source Window (Menu->Data->Show Data Source) and check the schema of the SharePoint List Data.

As same as other data sources we introduce before, we can drag the SharePoint Data Source from Data Source Window and drop to WPF Designer. It will generate the UI with Data Binding automatically.

If you open the code-behind file (MainWindow.xaml.vb), you will notice Visual Studio has already generated the sample code so that you can simply follow the comment to fill the data.
Private Sub Window_Loaded(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles MyBase.Loaded
Private Sub Window_Loaded(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles MyBase.Loaded
Dim AnnouncementsViewSource As System.Windows.Data.CollectionViewSource = CType(Me.FindResource("AnnouncementsViewSource"), System.Windows.Data.CollectionViewSource)
'Load data by setting the CollectionViewSource.Source property:
'AnnouncementsViewSource.Source = [generic data source]
End Sub
As we mentioned before, accessing SharePoint List Data is the same as other ADO.Net Data Service. The following is a sample code to load data using the default credential.
Private Sub Window_Loaded(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles MyBase.Loaded
Dim AnnouncementsViewSource As System.Windows.Data.CollectionViewSource = CType(Me.FindResource("AnnouncementsViewSource"), System.Windows.Data.CollectionViewSource)
'Load data by setting the CollectionViewSource.Source property:
Dim Context As New SharePointReference.TeamSiteDataContext(
New Uri("http://vspro-vm-bdc1/_vti_bin/ListData.svc", UriKind.Absolute))
Context.Credentials = System.Net.CredentialCache.DefaultCredentials
AnnouncementsViewSource.Source = Context.Announcements.Execute().ToList()
End Sub
So far we have finished the simple application with the SharePoint List Data Source. Cheers!
-
WPF Data Binding: How to Bind DataSet to WPF Designer
In our previous posts, we mostly used the Entity Data Model as the ORM (Object-relational mapping) layer for the WPF Data Binding scenarios.
Meanwhile, since DataSet is used broadly, we also provide the DataSet as a supported DataSource in the Data Binding stories. Following is a step-by-step example using C#. The example is based on Visual Studio 2010 Beta2 version.
Bind DataSet to WPF designer:
1. Open Visual Studio. From the main menu: File->New Project, choose WPF Application to create a new C# application. Here, we assume the project name to the default one: WpfApplication1.
2. From the main menu: Data->Add New Data Source…->DataBase->Dataset to trigger Data Source Wizard.
3. Follow the Wizard to connect to a Northwind SQL Server database, and choose tables: Customers, Orders. Following is what the Dataset designer looks like after this step finished:
4. From the main menu: Data->Show Data Sources to show the Data Sources Tool Window, and make sure that MainWindow.xaml is the active window:
5. Switch back to DataSources Tool Window, you can click on Customers node and click the drop-down menu. On the context menu, choose “Customize…” to popup the “Customize Control Binding” dialog. Select the ComboBox for the [List] Data type:
6. Bind the Customers to the “Combobox” control. After above steps, following is the snapshot before the drag&drop operation. From its icon, you can see the Customers is bound to Combobox now.
7. Drag&drop the Customers table to the top-left corner. Then, drag&drop the Customers.Orders to the center of the WPF designer. To layout the controls as follow:
With these steps, a simple DataSet Master-Details application is created. Press “Ctrl + F5” to run the application and see the data.
Behind the Scenes:
In step 5, there are two parts of code generated: One is the xaml markup code and the other is the C# code behind. The generated code is for demonstration purpose. Here, I will explain their corresponding meanings to provide guideline for modifying the code.
Regarding the xaml code, besides the markups for the controls/data-binding, which are the same when using the EDM as the ORM layer, it also generates resources section for the Window:
<Window.Resources>
<my:NorthwindDataSet x:Key="NorthwindDataSet" />
<CollectionViewSource x:Key="customersViewSource" Source="{Binding Path=Customers, Source={StaticResource NorthwindDataSet}}" />
<CollectionViewSource x:Key="customersOrdersViewSource" Source="{Binding Path=FK_Orders_Customers, Source={StaticResource customersViewSource}}" />
</Window.Resources>
The first line defines an object resource of the NorthwindDataSet instance, which is specific to DataSet. So when a Window is initialized, there will be a DataSet instance created. The 2nd and 3rd lines of code are doing master-details data-binding leveraging CollectionViewSource.
The generated C# code behind is:
private void Window_Loaded(object sender, RoutedEventArgs e)
{
WpfApplication1.NorthwindDataSet NorthwindDataSet = ((WpfApplication1.NorthwindDataSet)(this.FindResource("NorthwindDataSet")));
// Load data into the table Customers. You can modify this code as needed.
WpfApplication1.NorthwindDataSetTableAdapters.CustomersTableAdapter northwindDataSetCustomersTableAdapter = new WpfApplication1.NorthwindDataSetTableAdapters.CustomersTableAdapter();
northwindDataSetCustomersTableAdapter.Fill(NorthwindDataSet.Customers);
System.Windows.Data.CollectionViewSource customersViewSource = ((System.Windows.Data.CollectionViewSource)(this.FindResource("customersViewSource")));
customersViewSource.View.MoveCurrentToFirst();
// Load data into the table Orders. You can modify this code as needed.
WpfApplication1.NorthwindDataSetTableAdapters.OrdersTableAdapter northwindDataSetOrdersTableAdapter = new WpfApplication1.NorthwindDataSetTableAdapters.OrdersTableAdapter();
northwindDataSetOrdersTableAdapter.Fill(NorthwindDataSet.Orders);
System.Windows.Data.CollectionViewSource customersOrdersViewSource = ((System.Windows.Data.CollectionViewSource)(this.FindResource("customersOrdersViewSource")));
customersOrdersViewSource.View.MoveCurrentToFirst();
}
Again, the 1st line is getting the DataSet object instance from the WPF designer resource dictionary. The 2nd line is loading the Customers data into the table. You can replace it with other DataSet object instance as you prefer. The 3rd and 4th lines are initializing the CollectionViewSource. The lines 5~7, which are for the table Customers.Orders, are similar to lines 2~4.
You may notice that the data is loaded when the application starts. If you prefer to load it on demand, you can move this code to some other event handler such as a button click or something like that.
Hope this helps.
-
In Part 1 of this post, we developed our Silverlight application to access data through an ADO.NET Data Service. So far we are able to load the data at run time and navigate through the data records back and forth. Let’s now develop more functionality to update data, delete records, and add new records.
Updating Data Records
Our data is accessed through the Service Reference we added to our Silverlight application. However, the Service Reference does not keep track of the changes that we make in the application. This is actually due to a limitation of ADO.NET Data Service Client Library. We have to have a mechanism to notify the DataContext object about changes. In Part 1, we added the data we retrieved through the service into an ObservableCollection<T> so that we can get notifications when data get added, updated or removed. We can then notify the DataContext by implementing System.ComponentModel.INotifyPropertyChanged on the DataContract classes.
For example, in order to notify NorthwindEntities (the DataContext object) about changes of the CompanyName property of Customer (the DataContract object), we can do the following:
public partial class Customer : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
partial void OnCompanyNameChanged()
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("CompanyName"));
}
}
}
The OnCompanyNameChanged() partial method is defined (yet not implemented) in the generated Reference.cs file of the Service Reference. You can find this file in Solution Explorer under “Service References” – “NorthwindServiceReference” – “Reference.datasvcmap” – “Reference.cs”.
For this application, we are creating a class file “NorthwindEntitiesChangeManager.cs” in the Silverlight application. For every updatable property of Customer, Order, and Order_Details, we implement the On<PROPERTY>Changed partial method in the same manner as above.
We’ll also need to notify the DataContext object about DataContract object changes. Therefore, we are modifying the OnDataLoadComplete method to hook up the change events:
private void OnDataLoadComplete(IAsyncResult result)
{
List<Customer> custs = qry.EndExecute(result).ToList();
foreach (Customer cust in custs)
{
cust.PropertyChanged += new PropertyChangedEventHandler(cust_PropertyChanged);
foreach (Order ord in cust.Orders)
{
ord.PropertyChanged += new PropertyChangedEventHandler(ord_PropertyChanged);
foreach (Order_Details od in ord.Order_Details)
{
od.PropertyChanged += new PropertyChangedEventHandler(od_PropertyChanged);
}
}
data.Add(cust);
}
cvs.Source = data;
}
Handling the PropertyChanged events is to call UpdateObject() method on the DataContext object:
void cust_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
this.ctx.UpdateObject(sender as Customer);
}
void ord_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
this.ctx.UpdateObject(sender as Order);
}
void od_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
this.ctx.UpdateObject(sender as Order_Details);
}
Deleting Data Records
To delete records, we need to call the DeleteObject() method on the DataContext object.
The following code deletes the current Customer, and all the related Orders and Order_Details, similar to a CASCADE deletion on the database backend:
void DeleteItem_Click(object sender, RoutedEventArgs e)
{
string msg = "Are you sure you want to delete the current customer and all the related order information?";
if (HtmlPage.Window.Confirm(msg))
{
Customer cust = cvs.View.CurrentItem as Customer;
foreach (Order ord in cust.Orders)
{
foreach (Order_Details od in ord.Order_Details)
{
this.ctx.DeleteObject(od);
}
this.ctx.DeleteObject(ord);
}
this.ctx.DeleteObject(cust);
this.data.Remove(cust);
this.bindingNavigator1.RecordCount--;
this.bindingNavigator1.RefreshControls();
}
}
Similarly, we can delete the current selected Order and its Order_Details using the following code:
private void btnDeleteOrder_Click(object sender, RoutedEventArgs e)
{
string msg = "Are you sure you want to delete the current order and all the order details?";
if (HtmlPage.Window.Confirm(msg))
{
Order ord = this.ordersDataGrid.SelectedItem as Order;
int curOrderID = ord.OrderID;
foreach (Customer cust in this.data)
{
if (cust.CustomerID == ord.CustomerID)
{
foreach (Order o in cust.Orders)
{
if (o.OrderID == curOrderID)
{
List<Order_Details> order_DetailsToRemove = new List<Order_Details>();
foreach (Order_Details od in o.Order_Details)
{
ctx.DeleteObject(od);
order_DetailsToRemove.Add(od);
}
foreach (Order_Details odToRemove in order_DetailsToRemove)
{
o.Order_Details.Remove(odToRemove);
}
ctx.DeleteObject(o);
cust.Orders.Remove(o);
break;
}
}
break;
}
}
}
}
And here’s the code to delete the currently selected Order_Details:
private void btnDeleteOrder_Detail_Click(object sender, RoutedEventArgs e)
{
string msg = "Are you sure you want to delete the current order detail?";
if (HtmlPage.Window.Confirm(msg))
{
Order_Details curOrder_Detail = this.order_DetailsDataGrid.SelectedItem as Order_Details;
ctx.DeleteObject(curOrder_Detail);
Order curOrder = this.ordersDataGrid.SelectedItem as Order;
int curOrderID = curOrder.OrderID;
foreach (Customer cust in this.data)
{
if (cust.CustomerID == curOrder.CustomerID)
{
foreach (Order o in cust.Orders)
{
if (o.OrderID == curOrderID)
{
o.Order_Details.Remove(curOrder_Detail);
break;
}
}
break;
}
}
}
}
Adding New Data Records
To add new records, we can call the Create<OBJECT> method on the DataContract object, and then call the AddTo<OBJECTS> method of the DataContext object. For example, to add a new Customer, we can use the following:
Customer cust = Customer.CreateCustomer("ABCDE", "ABCDE Corp");
this.ctx.AddToCustomers(cust);
By drag-dropping data sources from the Data Sources window, we can easily create new data entry UI as Silverlight controls. This is the new data entry UI I created by stripping data binding XAML from the generated UI:
The Orders table in the database has OrderID as an IDENTITY field (auto-increment), so we can create a new Order object by setting -1 as OrderID:
Order ord = Order.CreateOrder(-1);
Customer cust = this.cvs.View.CurrentItem as Customer;
ord.CustomerID = cust.CustomerID;
// Set other properties …
cust.Orders.Add(ord);
this.ctx.AddToOrders(ord);
Here’s the code to create a new Order_Details:
Order ord = this.ordersDataGrid.SelectedItem as Order;
int productID = Int32.Parse(this.order_DetailsDataEntry.productIDTextBox.Text); //32
decimal unitPrice = Decimal.Parse(this.order_DetailsDataEntry.unitPriceTextBox.Text);
short quantity = short.Parse(this.order_DetailsDataEntry.quantityTextBox.Text); //5
float discount = float.Parse(this.order_DetailsDataEntry.discountTextBox.Text); //0.25f
Order_Details od = Order_Details.CreateOrder_Details(ord.OrderID, productID, unitPrice, quantity, discount);
ord.Order_Details.Add(od);
this.ctx.AddToOrder_Details(od);
Saving Changes
To save changes, we can simply call the SaveChanges() method on the DataContext object. The code is similar to the following (note you may want add code to handle data update exceptions not done here):
void SaveItem_Click(object sender, RoutedEventArgs e)
{
ctx.BeginSaveChanges(System.Data.Services.Client.SaveChangesOptions.Batch, new AsyncCallback(OnSaveChangesComplete), null);
}
void OnSaveChangesComplete(IAsyncResult result)
{
ctx.EndSaveChanges(result);
}
Summary
Up to this point, we have developed a Silverlight application capable of loading data through an ADO.NET Data Service, navigating through the data records, performing updating/deleting/inserting operations, and finally committing changes to the database backend. Data validation and error handling during save is not handled here but users can easily hook up their own business logic.
Hope you will enjoy developing data applications with Silverlight.
-
With Visual Studio 2010 Beta2, users can add data sources to a Silverlight application using the Data Source Configuration Wizard. The added data source is shown in the Data Sources tool window, and can be drag-dropped to Silverlight designer, with data-bound controls automatically generated.
In this post, I’ll walk you through the steps of creating an ADO.NET Data Service to access data using Entity Data Model, and creating a Silverlight application to consume the data service. We’ll build a classic Northwind order management application where users can do CRUD operations on customer and order information through the service. The completed application will look like this:
Note: Although the following walkthrough uses Visual C# as the programming language, you can complete it in VB as well to achieve the same functionalities.
Creating an ADO.NET Data Service
To create an ADO.NET Data Service, we will need to first create a new ASP.NET Web Application to host the service. Here are the steps to create the project, define an ADO.NET Entity Data Model, and add ADO.NET Data Service:
1. Select Visual Studio File->New Project menu, and choose ASP.NET Web Application.
2. From Solution Explorer, right click the Web Application project node, and choose Add New Item. In the Add New Item dialog, choose ADO.NET Entity Data Model under the Data category. This will invoke the Entity Data Model Wizard.
3. Follow the Entity Data Model Wizard to connect to a Northwind SQL Server database, and choose tables: Customers, Orders, and Order_Details.
4. From Solution Explorer, right click the Web Application project node, and choose Add New Item. In the Add New Item dialog, choose ADO.NET Data Service under the Web category.
5. The WebDataService1.svc.cs (or WebDataService1.svc.vb) file will be opened in VS code editor. We’ll need to modify the code as follows to expose the data model and set appropriate access rules. Note we have changed the default EntitySetRights from AllRead to All so that we are able to update the entities later.
public class NorthwindWebDataService : DataService< NorthwindEntities >
{
public static void InitializeService(DataServiceConfiguration config)
{
config.SetEntitySetAccessRule("*", EntitySetRights.All);
config.SetServiceOperationAccessRule("*", ServiceOperationRights.All);
config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
}
}
6. Build the project.
At this point, our Data Service is ready to be consumed by client applications.
Creating a Silverlight Application to Consume the ADO.NET Data Service
Now let’s create a Silverlight Application to consume the data service. The following steps create a Silverlight application, and add a new data source to consume the above ADO.NET Data Service:
1. From Visual Studio File->Add New Project menu, choose Silverlight Application.
2. On the New Silverlight Application dialog, accept the default to host the Silverlight Application in the above ASP.NET Web Application.
3. From Visual Studio Data menu, choose Add New Data Source.
4. On the Data Source Configuration Wizard, choose Service as the data source type, and click Next.
5. This will bring up the Add Service Reference dialog. Since our data service is in the same solution, we can click the Discover button to find the above ADO.NET Data Service, or type in the service address (like http://localhost:<PORT NUMBER>/WebDataService1.svc), and click OK.
6. Finish the Data Source Configuration Wizard. A new Service Reference will be added in Solution Explorer, with a code file containing data classes used to access and interact with data service resources as objects.
Drag-dropping Data Sources onto Silverlight Designer
In the above steps, we have added a Service data source to the Silverlight application. If you open the Data Sources tool window, the data objects accessible through the ADO.NET Data Service should be shown and available for drag-dropping to the Silverlight designer. However, due to a bug not yet fixed, you may need to close VS and then reopen it (this bug applies only to data services created within the same solution. If you have an existing data service, the Data Sources window will show the data objects immediately).
1. From Visual Studio Data menu, choose Show Data Sources. The Data Sources tool window will show up with data objects from the ADO.NET Data Service: Customers, Orders, and Order_Details.
2. In the Northwind Order Manager application, we want to show the customer information as details rather than the default DataGrid. To do this, in the Data Sources window, drop-down the Customers ComboBox, and choose Details.
3. Now drag Customers node to MainPage.xaml designer. The customer information is generated on the designer as data-bound controls. The XAML code also contains data binding information similar to the following:
<CollectionViewSource x:Key="customersViewSource" d:DesignSource="{d:DesignInstance my:Customer, CreateList=True}" />
4. To show the related Orders and Order_Details information, drag Customers.Orders (IMPORTANT: this is the child node Orders, under Customers node) and Customers.Orders.Order_Details from Data Sources Window to MainPage.xaml designer. The generated CollectionViewSource nodes in the XAML file will be similar to the following:
<CollectionViewSource x:Key="customersOrdersViewSource" Source="{Binding Path=Orders, Source={StaticResource customersViewSource}}" />
<CollectionViewSource x:Key="customersOrdersOrder_DetailsViewSource" Source="{Binding Path=Order_Details, Source={StaticResource customersOrdersViewSource}}" />
Writing Code to Load Data at Run Time
We’ll need to write some code to load the data at run time. Let’s declare some private members in the MainPage class in MainPage.xaml.cs:
private NorthwindEntities ctx; // The data context
private CollectionViewSource cvs; // The CollectionViewSource to bind data
private DataServiceQuery<Customer> qry; // The query to load data
private System.Collections.ObjectModel.ObservableCollection<Customer> data = new System.Collections.ObjectModel.ObservableCollection<Customer>();
In the MainPage.xaml.cs LayoutRoot_Loaded event handler, add the following code to instantiate the data service, define a DataServiceQuery and execute it to retrieve data. The retrieval of data is done through an asynchronous call.
private void LayoutRoot_Loaded(object sender, RoutedEventArgs e)
{
ctx = new NorthwindEntities(new Uri("http://localhost:<PORT NUMBER>/NorthwindWebDataService.svc"));
cvs = this.Resources["customersViewSource"] as CollectionViewSource;
qry = ctx.Customers.Expand("Orders").Expand("Orders/Order_Details");
qry.BeginExecute(new AsyncCallback(OnDataLoadComplete), null);
}
private void OnDataLoadComplete(IAsyncResult result)
{
List<Customer> custs = qry.EndExecute(result).ToList();
foreach (Customer cust in custs)
{
data.Add(cust);
}
cvs.Source = data;
}
Note we are using an ObservableCollection<T> to be the Source of the CollectionViewSource, rather than just a List<T> or DataServiceQuery<T> . This is so that we can get notifications when data get added, updated or removed since we are going to update and save data later. If you do not need to update data, you may just need to set the CollectionViewSource.Source to the DataServiceQuery like :
cvs.Source = ctx.Customers.Expand("Orders").Expand("Orders/Order_Details");
Navigating through Data
To add the functionality of navigating through data at run time, we can create a Silverlight User Control similar to the following:
The XAML for one of the buttons is similar to this:
<Button Margin="5" IsEnabled="False" Name="moveFirstItem" Click="moveFirstItem_Click">
<Image Height="23" Name="moveFirstItemImage" Source="BindingNavigator_Icons/MoveFirstItem.ico">
<Image.OpacityMask>
<ImageBrush>
<ImageBrush.ImageSource>
<BitmapImage UriSource="BindingNavigator_Icons/MoveFirstItem.ico" />
</ImageBrush.ImageSource>
</ImageBrush>
</Image.OpacityMask>
</Image>
</Button>
To move to the beginning of the data collection, we can handle the MoveFirstItem button Click event with the following code:
private void moveFirstItem_Click(object sender, RoutedEventArgs e)
{
cvs.View.MoveCurrentToFirst();
RefreshControls();
}
The RefreshControls() method handles the logic to update the current position display and enable or disable buttons based on the current record position:
private void RefreshControls()
{
int pos = cvs.View.CurrentPosition;
this.positionItem.Text = (pos + 1).ToString(System.Globalization.CultureInfo.CurrentCulture);
this.countItem.Text = String.Format(this.countItemTemplate, this.recordCount);
this.moveFirstItem.IsEnabled = (pos > 0);
this.movePreviousItem.IsEnabled = (pos > 0);
this.moveNextItem.IsEnabled = (pos < this.RecordCount - 1);
this.moveLastItem.IsEnabled = (pos < this.RecordCount - 1);
this.deleteItem.IsEnabled = (this.RecordCount > 0);
}
Similarly, we can use the following code to navigate through the records by moving back and forth:
private void movePreviousItem_Click(object sender, RoutedEventArgs e)
{
cvs.View.MoveCurrentToPrevious();
RefreshControls();
}
private void moveNextItem_Click(object sender, RoutedEventArgs e)
{
cvs.View.MoveCurrentToNext();
RefreshControls();
}
private void moveLastItem_Click(object sender, RoutedEventArgs e)
{
cvs.View.MoveCurrentToLast();
RefreshControls();
}
We also provide the functionality to navigate to a specific position using the TextBox to accept user input, and navigate when the user presses the ENTER key with a valid position entered. If the number entered is not valid, it will just be ignored and the record stays at the current position. If the number is smaller than 1, we navigate to the first record instead. If the number entered is greater than the record count, we navigate to the last record.
private void positionItem_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
TextBox txt = sender as TextBox;
int curPos = this.cvs.View.CurrentPosition + 1;
int pos;
bool isValid = int.TryParse(txt.Text, out pos);
if (!isValid)
{
txt.Text = curPos.ToString();
}
else
{
if (pos <= 1)
{
pos = 1;
}
else if (pos > this.recordCount)
{
pos = this.recordCount;
}
if (curPos != pos)
{
this.cvs.View.MoveCurrentToPosition(pos - 1);
}
txt.Text = pos.ToString();
RefreshControls();
}
}
}
Next Steps
The next part of this post will add functionalities to update and save Customers, Orders and Order_Details. We will also develop the application to be able to delete data and add new records.
-
Visual Studio 2010 Beta 2 has been released! Check out the Visual Studio 2010 and .NET Framework 4 Beta 2 site to download the Beta, submit product feedback on the Beta Forums, report bugs on Connect, and watch videos about Visual Studio 2010 on Channel 9. Also visit the Beta 2 walkthroughs page for information on how to use the new features and download the Visual Studio 2010 Samples. There’re also some good language-focused resources on the Visual Basic 2010 and C# 2010 that are hanging off the Developer Centers.
We have blogged a lot on the drag-drop data binding experience in WPF application. With the Beta 2 release, you could get the similar RAD experience in Silverlight application! We will blog more in this area, so please stay tuned!
If you are an Office developer, please check out the Advanced Office Solution Deployment topic in the library and the VSTO Team blog for some cool new Office client features like Deploying Multiple Office Solutions in a Single ClickOnce Installer and Copying a Document to the End User Computer after a ClickOnce Installation. And don’t forget to check the Office Development in Visual Studio Developer Center for updates.
Take a look through the Visual Studio 2010 Product Highlights and What's New in Visual Studio 2010 .
Visual Studio 2010 Beta 2 resources:
Oh, we’re also announcing the official launch date of Visual Studio 2010 and .NET Framework 4.0!
-
It’s been debatable whether to include FK (foreign key) columns in the conceptual model. That’s why in Visual Studio 2010, a checkbox is added to Entity Data Model Wizard, so that the decision is left to the developers. While I was playing with it yesterday, I found FK particularly useful in the following scenario.
The application I was trying to build is really simple: change an order’s related customer from A to B.

If FK (CustomerID) is in my Order entity, I could just do a lookup binding to this FK column, and call dataContext.SaveChanges().The drag-n-drop way of doing lookup binding to a ComboBox is to first drag the FK property (in this example, CustomerID in Order entity) with a ComboBox control, and drop it onto the form. Then drag the related lookup table (Customer entity) and drop it onto the previously created ComboBox control. The SelectedValue, SelectedValuePath, ItemsSource, and DisplayMemeberPath properties will be set for you.
Things become a little bit tricky if I don’t have the FK in the entity.
Data bind Orders to a DataGrid
This step is fairly easy. Just drag Orders node onto the WPF Designer.

Create lookup binding to a ComboBox
Because I don’t have the FK column in the model, there’s no CustomerID property in Order entity. To create the lookup binding, I need to expand the navigation property Customer in Orders, and bind the CustomerID to a combobox.

After I drag-n-drop this node onto WPF Designer, I will have created a complex binding. To make it a lookup binding, I will link the newly created combobox to the lookup table. The way to do it is to drag the Customers node (marked with 2 on the picture above), and drop it onto the combobox. Please notice that while doing this, your cursor will change from a little + to an arrow, to indicate that we will not generate a new control. Instead, an existing control will be updated with the dragged data source.
Create button to save changes
Then I will add a button to “Update Customer”. In the click event handler, I need to find current selected order, get the new CustomerID that replace the old FK value, and save the changes:
Order currentOrder = (Order)ordersViewSource.View.CurrentItem;
string customerID = customerIDComboBox.SelectedValue.ToString();
currentOrder.CustomerReference.EntityKey.EntityKeyValues[0].Value = customerID;
northwindEntities.SaveChanges();
I made the data context (northwindEntities) and colletion view source (ordersViewSource) to be the property in MainWindow class, to make the code simple. I need to drill down into the order, find the CustomerReference, and set the EntityKeyValue.
F5
Debug the application now. Everything looks good at first, but when I change the customer from one to another in the drop-down combobox, Bang! I get an InvalidOperationException: The property 'CustomerID' is part of the object's key information and cannot be modified. Although I successfully created the lookup binding, I cannot modify the value because the SelectedValue is not the FK in Order, but the PK in Customer!
This tells me that the combobox should be bound OneWay. I locate the combobox in XAML, and updated the SelectedValue binding mode:
SelectedValue="{Binding Path=Customer.CustomerID,Mode=OneWay}"
F5 again
Now change selection in the combobox will not trigger the exception anymore. I click on the “Update Customer” button to continue testing, bang (again)! This time, it says EntityKey values cannot be changed once they are set. I don’t believe there’s no way to do it, so I searched around the web and found that although the value cannot be reset, the entity key can be. Now let’s assign the FK value in this way:
currentOrder.CustomerReference.EntityKey = new System.Data.EntityKey(
currentOrder.CustomerReference.EntityKey.EntityContainerName+"."+currentOrder.CustomerReference.EntityKey.EntitySetName,
"CustomerID", customerID);
You could also use KeyValuePair if you have a complex key composed of more than one property. For more information, please see here.
After all the steps above, my application finally works as I expected. You might ask, why don’t you just include the key to save all these coding and bang (runtime exception)? Some developers, as I said, vote for not including FK in the conceptual model because FK is a relational database concept and it will pollute the object world. Another reason is, the feature of include FK is only available in Visual Studio 2010, targeting .NET framework 4.0, for newly added tables. If you have an existing entity data model, you would still need to get yourself used to the life without FK.
Please let me know if you have any other idea or question about this!
Cheers!
[Update 10/20]: Alex has a post talking about this too. He suggested a way of wrapping the entity key so that a FK property is added into the partial class. Please check it out here: http://msdn.microsoft.com/en-us/vbasic/bb735936.aspx
-
If you’ve been using Data Source Configuration Wizard since Visual Studio 2005, you’ll notice that with Visual Studio 2008 Service Pack 1, there’s a “small” change – on Choose Your Database Objects page, a new checkbox would allow you to “enable local database caching”:

This checkbox will be available if the current connection is to a SQL Server database. The technology behind is Microsoft Synchronization Service for ADO.NET, which lets you synchronize data among any kind of data sources that support ADO.NET. If your client application does not always have a network connection, or if the application requires good data loading performance regardless of the network condition, you should consider using this feature to create a local copy of the data on the client and have your DataSet work against this local data cache. Another benefit is scalability. You can use local database caches for read-only data or rarely-changing data to increase database scalability. For more information of the technology, please click here.
If this checkbox is checked, the next page will let you choose among all the data tables in the DataSet, which tables you’d like to cache.

Something worth mentioning on this page are:
- Not all tables can be synchronized. Tables that don’t have a primary key, for example, cannot be synchronized with the wizard.
- There’re two synchronization modes: Incremental mode synchronizes only changes that were made on the server since the last synchronization. Snapshot mode replaces the whole locally cached table with the current table on the server.
- If you’re using SQL Server 2008, another checkbox will let you take advantage of the change tracking feature. With this feature enabled, for incremental mode of synchronization, you don’t need to create timestamp column or tombstone table [BAM3] to track all the changes since the last synchronization (timestamp columns will track each data item’s insert and update time; a tombstone table will track each data item’s delete time) . SQL Server 2008 will do it for you.
After selecting these settings, the Data Sources Configuration Wizard will prompt you to generate and run a script if you have tables with incremental mode synchronization. The script will help you turn on the SQL Server 2008 change tracking feature, or if you didn’t enable this feature, the script will help you generate all the necessary columns or table to track insert, update and delete operations since last sync.

Now a DataSet, a sync file, and a local database cache are added into the project. The DataSet will have two connection strings, one to the remote database, another to the local database cache. The application works fine at first because the server tables and client tables have identical data. However, of course, sooner or later, the two databases will be out of sync. To synchronize the local database cache with the remote one is pretty simple: call Synchronize method of the SyncAgent. For more information, please check this walkthrough: Creating an Occasionally Connected Application by Using the Data Source Configuration Wizard.
Please pay attention if you reopen the Data Source Configuration Wizard and make updates to the synchronization configuration. It might cause the DataSet to be regenerated. If you have any customization to the DataSet, please remember to back it up beforehand.
Enjoy coding and syncing!
Cheers!
-
If you have an identity primary key column defined in a database table, its value will be automatically set by the database engine when you insert a new row to the table. The identity value is determined by the column’s Identity Seed and Identity Increment properties.
While in your client application, you can insert a new row into a corresponding ADO.NET DataTable (DataTable can be seen as the in memory cache of the data table in the database, I will use DataTable as the example in this post). You will not get the true value until you commit the update to the database. What you see before the update is not what you get after the update. For better user experience, you really want to refresh the database to retrieve the correct identity value back right after the insert operation.
In this post, I will describe the way to retrieve the identity value in ADO.NET applications with the help of Visual Studio Database Tools. I will start with a sample Windows Forms application using DataSet. My database server is SQL Server.
First in Visual Studio (2005 and after) server explorer, I establish a data connection connects to a SQL Server database. I create a data table in the database called MyCustomer with three columns: CustId, Name and Company. CustId is defined as primary key and an identity column. Identity Seed and Identity Increment are set as 1 by default as shown in Figure 1 below.
Figure 1 MyCustomer table defined in database
Then I create my DataSet through the Data Source Configuration Wizard, or by adding a DataSet and then dragging and dropping MyCustomer from Server Explorer to the DataSet Designer. By opening the DataSet and selecting the CustId column, you will see that it has the following properties: AutoIncrement=True, and AutoIncrementSeed = -1 , AutoIncrementStep = -1 (Figure 2). These properties are used by ADO.NET to auto generate the CustId’s placeholder values for newly added rows.

Figure 2. CustId properties in the DataTable
The AutoIncrement property corresponds to the Identity property in the database. AutoIncrementSeed corresponds to Identity Seed, and AutoIncrementStep to Identity Increment. You may be curious why both AutoIncrementSeed and AutoIncrementStep are set to -1. This is an approach to ensure that the placeholder values that ADO.NET generates will not conflict with values that already exist in the database. Another benefit is it looks like unreal so users will know that it is just a temporary placeholder value.
Now if you click on the MyCustomerTableAdapter header and show its CommandText for Insert Command, you will see this:
INSERT INTO [MyCustomer] ([Name], [Company]) VALUES (@Name, @Company);
SELECT CustId, Name, Company FROM MyCustomer WHERE (CustId = SCOPE_IDENTITY())
The command text contains two statements and the second one is used to retrieve the primary key value after the insert operation is committed. Notice the use of the SCOPE_IDENTITY function, which is defined so in MSDN: “SCOPE_IDENTITY (Transact-SQL) Returns the last identity value inserted into an identity column in the same scope. A scope is a module: a stored procedure, trigger, function, or batch. Therefore, two statements are in the same scope if they are in the same stored procedure, function, or batch”
The auto generation of the second statement in Insert command is controlled by the “refresh data table option” in the TableAdapter Configuration Wizard (Figure 3).
Figure 3. Refresh the data table advanced option in the TableAdapter Configuration Wizard
Now let’s take a look what is the experience when applying this refresh data table feature at run time. Let me open the Form and then show the Data Sources Window. From the Data Sources Window, I drag and drop the MyCustomer table to the Form and I get the following layout (Figure 4):
Figure 4. At design time: Drag and Drop MyCustomer from the Data Sources window to the form
Hit F5 to run the app. Click on the Add New (the + sign) button to add some rows. Notice that I will get values of -1, -2, -3, etc. for CustId column.
Figure 5. CustForm at run time, before committing
Now I click the Save (the disk sign) button and I see that the CustIds are updated to 9,10,11. Oh! I would expect to get 2,3,4; someone may beat me and add some rows before me. You see that using -1,-2,-3 really make sense here to clearly indicate that the values are not committed.
Figure 6. CustForm at run time, after committing
Now if you are using MS Access database or SQL CE, you will see that the above walk through does not work as expected. When you click the save, the primary key -1,-2,-3 keep unchanged. If you check on the TableAdapter Configuration Wizard, the Refresh the data table option is disabled. If you check the generated insert command, there is only one statement. This is because MS Access database and SQL CE do not support batching SQL statements and therefore cannot use the SCOPE_IDENTITY function. The build-in refresh the data table option is not available for these databases.
The good news is there is a sound workaround to fix this by reset the primary key identity value on Adapter.RowUpdated event. Please see Beth Massi’s blog: Using TableAdapters to Insert Related Data into an MS Access Database. Beth promised to write a follow up post for SQL CE as well. J
In summary, SQL server or any database supporting batching operation has a reliable way to retrieve the identity value through the usage of the SCOPE_IDENTITY function. Visual Studio Data Tool provides the auto generated Insert command that leverages this support with the “Refresh the data table” option turn on by default. For databases that do not support batching of SQL statements, resetting the primary key value on row updated event is a good workaround.
-
In the MSDN Visual Basic Power Packs forum, I had an embarrassing reply on a DataRepeater event handling question, in which the user asked why his event handling code did not work. Here is the original question:
“I want to be able to resize a textbox when a user clicks on a textbox within a datarepeater. So the textbox will start out as a single line, but would expand to be multiline to better allow the user to enter text. When the user's mouse leaves the textbox I want it to return to the original size. I tried changing the textbox.height, but nothing happens. Any help is appreciated.”
By looking at the question, I guessed that the user might forget to set the TextBox.MultiLine to true while trying to change the height. Without hesitation, I replied a sample code as below:
Private Sub TextBox1_MouseEnter(ByVal sender As Object, ByVal e As System.EventArgs) Handles TextBox1.MouseEnter
Me.TextBox1.Multiline = True
Me.TextBox1.Height = 50
End Sub
Private Sub TextBox1_MouseLeave(ByVal sender As Object, ByVal e As System.EventArgs) Handles TextBox1.MouseLeave
Me.TextBox1.Multiline = False
End Sub
Code Sample 1
The user then replied back that the Multiline was not the problem. My code did not work! What is happening?
The DataRepeater control is very intuitive to use and is close to the WYSIWYG experience. However, we need to remember that at run time, the repeater items are cloned using the repeater item template. At design time, we are working on the repeater item template. At run time, several sets of new controls are created to serve as the display of the visible repeater items. Public properties of a control are copied during the cloning process, so do the event handlers. If Button1 is a button in the data repeater item template, code sample 2 below works perfectly.
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
MessageBox.Show("Hello")
End Sub
Code Sample 2
Now let’s take a look on code sample 1, the event handler is cloned and so the code will be called when the mouse enters or leaves the TextBox. However, the code does not work as expected because TextBox1 refers to the template version of the TextBox, which is hidden at run time. The underline TextBox that triggers the event is the cloned version of the TextBox and in the context it is the sender object. So I modified code sample 1 to code sample 3, accessing TextBox1 through the sender object. Tested and worked.
Private Sub TextBox1_MouseEnter(ByVal sender As Object, ByVal e As System.EventArgs) Handles TextBox1.MouseEnter
Dim txtBox As TextBox = TryCast(sender, TextBox)
If (txtBox IsNot Nothing) Then
txtBox.Multiline = True
txtBox.Height = 50
End If
End Sub
Private Sub TextBox1_MouseLeave(ByVal sender As Object, ByVal e As System.EventArgs) Handles TextBox1.MouseLeave
Dim txtBox As TextBox = TryCast(sender, TextBox)
If (txtBox IsNot Nothing) Then
txtBox.Multiline = False
End If
End Sub
Code Sample 3
I hope this example can help you better understand how the DataRepeater works. Keep in mind that the controls in the repeater item area we work on at design time are used for templates and we should not use them at run time unless you want to change the properties for all repeater items (please see Gavin’s blog: How to Change Appearance of all DataRepeater Items at Run Time). If you need to work on an individual control on a repeater item, use the sender object (as in code sample 1) and then get its parent and siblings if necessary. You can also get access to a DataRepeaterItem, which inherits from Panel control, and then you can access to all the controls in the control’s hierarchical structure. To illustrate this, I would like to change the original forum question to resize the TextBox whenever the current repeater item changes. Code sample 4 below is my solution.
Dim lastModifiedTxtBox As TextBox = Nothing
Private Sub DataRepeater1_CurrentItemIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles DataRepeater1.CurrentItemIndexChanged
' Get The TextBox1 from Me.DataRepeater1.CurrentItem
Dim txtBox As TextBox = Me.DataRepeater1.CurrentItem.Controls("TextBox1")
If (txtBox IsNot Nothing) Then
txtBox.Multiline = True
txtBox.Height = 50
End If
If (Not Object.ReferenceEquals(txtBox, lastModifiedTxtBox)) Then
If (lastModifiedTxtBox IsNot Nothing) Then
lastModifiedTxtBox.Multiline = False
End If
lastModifiedTxtBox = txtBox
End If
End Sub
Code Sample 4
In code sample 4, I use DataRepeater.CurrentItem property to get the current DataRepeaterItem and then I use Control.Controls property to return the cloned version of the TextBox1 in the current DataRepeaterItem. Notice that the cloned version of the TextBox is named as “TextBox1” as well, this is allowed at run time as long as it is named uniquely under the parent control (the DataRepeaterItem, in the context). Because there is no CurrentItemIndexChanging event, I use lastModifiedTextBox to remember the last modified TextBox and restore its state when necessary.
Lastly, I would like to point out again (as I have in my previous post) that the DataRepeater control applies a virtualization technique to display the data. So at run time, only a few DataRepeaterItems are created for display purpose. These DataRepeaterItems are reused by all data rows when they enter the visible range. So it is important that if you set a property on a control based on a data row. You may want to reset it appropriately when the data row is out of the visible range as the control will be reused by other data rows. Let me change the task again to only resize the TextBox1 for the first data row, and I will use DataRepeater.DrawItem event this time to get the DataRepeaterItem.
Private Sub DataRepeater1_DrawItem(ByVal sender As Object, ByVal e As Microsoft.VisualBasic.PowerPacks.DataRepeaterItemEventArgs) Handles DataRepeater1.DrawItem
Dim txtBox As TextBox = e.DataRepeaterItem.Controls("TextBox1")
If (txtBox IsNot Nothing) Then
If (e.DataRepeaterItem.ItemIndex = 0) Then
' Special treatment for the first data row
txtBox.Multiline = True
txtBox.Height = 50
Else
' Reset to its original state..
' Without this line, the result will be unexpected.
txtBox.Multiline = False
End If
End If
End Sub
Code Sample 5
In code sample 5 above, if I omit the line to reset the txtBox.Multiline = False, you will see unexpected result after scrolling the data repeater up and down for a few times. See an illustration in picture 1 below.
Picture 1
In conclusion, writing event handler for a control in a DataRepeaterItem can be as straightforward as in a plain form like code sample 2. However, when individual control is needed, you need to make sure to get the correct run time control and remember to reset its property after the use.
-
Hierarchical update is an important feature of typed dataset that refers to the process of saving updated data back to a database while maintaining referential integrity rules. This feature is enhanced in Visual Studio 2008 by introducing a TableAdapterManger component to manage all TableApdaters in a typed dataset. When updating related tables, TableAdapterManager uses foreign-key relationships to determine the correct order to send Insert, Update, and Delete commands from a dataset to the database without violating the foreign-key constraints (referential integrity) in the database.
TableAdpaterManager is automatically generated when you create a typed-dataset in a project with Hierarchical Update enabled (For more information, see How to Enable and Disable Hierarchical Update), and it could greatly reduce the code to save data to multiple related tables. Users just need to call TableAdpaterManager.UpdateAll(DataSet). You could see how easy it is to use this class in “Sample Code Snippet to Update the Database” section of this article. However, when updating self-referencing tables, TableAdpaterManager has some defects in determining the correct order of affected rows, which could cause constraint violation errors when committing the changes to database. We have noticed this problem and are investigating how to resolve it. I’ll explain the idea of the solution and how to work around this issue with Visual Studio 2008 SP1.
What the Problem is
Sample Table
Suppose that you have a self-referencing table named Temp with three columns and a foreign-key relationship.
Table Definition:

Foreign-Key Relation:

Generated Typed-DataSet Code Snippet
Suppose you add a typed-dataset (say, dataset1) to your application, and drag-drop the Temp table from Server Explorer to the dataset designer, Visual Studio will generate code of the typed-dataset for you. Below is the code snippet that is related to this topic.
[C#]
public partial class TableAdapterManager : global::System.ComponentModel.Component {
public virtual int UpdateAll(DataSet1 dataSet) {
. . . . . .
this.UpdateInsertedRows(dataSet, allAddedRows));
. . . . . .
this.UpdateDeletedRows(dataSet, allChangedRows));
. . . . . .
}
private int UpdateInsertedRows(DataSet1 dataSet, global::System.Collections.Generic.List<global::System.Data.DataRow> allAddedRows) {
. . . . . .
this.SortSelfReferenceRows(addedRows, dataSet.Relations["FK_Temp_Temp"], false);
. . . . . .
}
private int UpdateDeletedRows(DataSet1 dataSet, global::System.Collections.Generic.List<global::System.Data.DataRow> allChangedRows) {
. . . . . .
this.SortSelfReferenceRows(addedRows, dataSet.Relations["FK_Temp_Temp"], true);
. . . . . .
}
protected virtual void SortSelfReferenceRows(global::System.Data.DataRow[] rows, global::System.Data.DataRelation relation, bool childFirst) {
global::System.Array.Sort<global::System.Data.DataRow>(rows, new SelfReferenceComparer(relation, childFirst));
}
private class SelfReferenceComparer : object, global::System.Collections.Generic.IComparer<global::System.Data.DataRow> {
private global::System.Data.DataRelation _relation;
private int _childFirst;
. . . . . .
public int Compare(global::System.Data.DataRow row1, global::System.Data.DataRow row2) {
if (object.ReferenceEquals(row1, row2)) {
return 0;
}
if ((row1 == null)) {
return -1;
}
if ((row2 == null)) {
return 1;
}
// Is row1 the child or grandchild of row2
if (this.IsChildAndParent(row1, row2)) {
return this._childFirst;
}
// Is row2 the child or grandchild of row1
if (this.IsChildAndParent(row2, row1)) {
return (-1 * this._childFirst);
}
return 0;
}
}
}
[VB]
Partial Public Class TableAdapterManager
Inherits Global.System.ComponentModel.Component
Public Overridable Function UpdateAll(ByVal dataSet As DataSet1) As Integer
. . . . . .
Me.UpdateInsertedRows(dataSet, allAddedRows)
. . . . . .
Me.UpdateDeletedRows(dataSet, allChangedRows)
. . . . . .
End Function
Private Function UpdateInsertedRows(ByVal dataSet As DataSet1, ByVal allAddedRows As Global.System.Collections.Generic.List(Of Global.System.Data.DataRow)) As Integer
. . . . . .
Me.SortSelfReferenceRows(addedRows, dataSet.Relations("FK_Temp_Temp"), False)
. . . . . .
End Function
Private Function UpdateDeletedRows(ByVal dataSet As DataSet1, ByVal allChangedRows As Global.System.Collections.Generic.List(Of Global.System.Data.DataRow)) As Integer
. . . . . .
Me.SortSelfReferenceRows(addedRows, dataSet.Relations("FK_Temp_Temp"), True)
. . . . . .
End Function
Protected Overridable Sub SortSelfReferenceRows(ByVal rows() As Global.System.Data.DataRow, ByVal relation As Global.System.Data.DataRelation, ByVal childFirst As Boolean)
Global.System.Array.Sort(Of Global.System.Data.DataRow)(rows, New SelfReferenceComparer(relation, childFirst))
End Sub
Private Class SelfReferenceComparer
Inherits Object
Implements Global.System.Collections.Generic.IComparer(Of Global.System.Data.DataRow)
Private _relation As Global.System.Data.DataRelation
Private _childFirst As Integer
. . . . . .
Public Function Compare(ByVal row1 As Global.System.Data.DataRow, ByVal row2 As Global.System.Data.DataRow) As Integer
Implements Global.System.Collections.Generic.IComparer(Of Global.System.Data.DataRow).Compare
If Object.ReferenceEquals(row1, row2) Then
Return 0
End If
If (row1 Is Nothing) Then
Return -1
End If
If (row2 Is Nothing) Then
Return 1
End If
'Is row1 the child or grandchild of row2
If Me.IsChildAndParent(row1, row2) Then
Return Me._childFirst
End If
' row2 the child or grandchild of row1
If Me.IsChildAndParent(row2, row1) Then
Return (-1 * Me._childFirst)
End If
Return 0
End Function
End Class
End Class
Sample Code Snippet to Update the Database
Then, you could update the table with the help of TableAdapterManager. Below is snippet of sample code to insert some rows to the self-referencing Temp table, and then delete them.
[C#]
DataSet1 ds = new DataSet1();
DataSet1.TempRow row1 = ds.Temp.AddTempRow(1, null, "Row 1");
DataSet1.TempRow row4 = ds.Temp.AddTempRow(4, null, "Row 4");
DataSet1.TempRow row2 = ds.Temp.AddTempRow(2, row1, "Row 2");
DataSet1.TempRow row3 = ds.Temp.AddTempRow(3, row4, "Row 3");
DataSet1TableAdapters.TableAdapterManager manager = new DataSet1TableAdapters.TableAdapterManager();
. . . . . .
manager.UpdateAll(ds); // Insert the rows
row1.Delete();
row2.Delete();
row3.Delete();
row4.Delete();
manager.UpdateAll(ds); // Delete the rows
[VB]
Dim ds As DataSet1 = New DataSet1
Dim row1 As DataSet1.TempRow = ds.Temp.AddTempRow(1, Nothing, "Row 1")
Dim row4 As DataSet1.TempRow = ds.Temp.AddTempRow(4, Nothing, "Row 4")
Dim row2 As DataSet1.TempRow = ds.Temp.AddTempRow(2, row1, "Row 2")
Dim row3 As DataSet1.TempRow = ds.Temp.AddTempRow(3, row4, "Row 3")
Dim manager As DataSet1TableAdapters.TableAdapterManager = New DataSet1TableAdapters.TableAdapterManager
manager.UpdateAll(ds) ' Insert the rows
row1.Delete()
row2.Delete()
row3.Delete()
row4.Delete()
manager.UpdateAll(ds) ' Delete the rows
What is the Problem and Why
From the code at the beginning of the post, we can see that TableAdapterManager uses System.Array.Sort<T>(T, IComparer<T>) to sort all rows before inserting them into or deleting them from database so that no violations to foreign-key constraints occur. The expected sorting result is: parent first for inserted rows, and child first for deleted rows. Take the sample code above for example, before calling SortSelfReferenceRows method, content of the array to insert is <R1, R4, R2, R3>. After calling the sorting method, R1 should come before R2, and R4 should come before R3. Because R1 is Parent of R2, and R4 is Parent of R3. Sequence between two rows that do not have Parent-Child (or Grandparent-Grandchild, etc) relationships are undetermined, e.g., R1 could come either before or after R4.
For each pair <row1, row2> in the array to sort, SortSelfReferenceRows.Compare() will be called to determine their sequence in the output array. From the implementation of SortSelfReferenceRows.Compare, we could see that any two rows that do not have a Parent-Child (or Grandparent-Grandchild, etc) relationship are treated equal. This is OK if all possible row pairs are compared, e.g. < R1, R4>, < R1, R2>, < R1, R3>, < R4, R2>, < R4, R3>, < R2, R3> are all explicitly compared by passing them to SortSelfReferenceRows.Compare. However, System.Array.Sort is implemented using QuickSort, it does not compare every possible row pair. Instead, equation relationships of rows are treated transitive. For example, if < R1, R4>is compared and the result is R1 == R4, < R4, R2> is compared and the result is R4 == R2, then System.Array.Sort will indicate that R1 == R2, and does NOT compare < R1, R2> at all! Clearly, you could see that R1 is Parent of R2, < R1, R2> should be explicitly compared and the result should be R1 < R2.
The Idea of the Solution
So, how to sort the array so that no foreign-key constraints are violated? The idea is to treat the array as a forest, group all rows that have the same root into a tree, and compare the rows based on their root and their distance to the root. For example, suppose we have an array < R1, R2, …, R14>, and the Parent-Child relationships among its elements are shown like picture below.
We still use System.Array.Sort<T>(T, ICompare<T>) to sort the array. That means we do not have direct control over the sorting algorithm, and we do not know what row pairs to be compared or the sequence of these comparisons. However, we do could impact the sorting process by controlling the comparison result of row pairs. That’s why we need to re-write the SelfReferenceComparer.Compare method.
The pseudo code of the new Compare method is (suppose Parent First):
int Compare (DataRow row1, DataRow row2)
DataRow root1 = row1’s root;
int distance1 = row1’s distance to root1
DataRow root2 = row2’s root;
int distance2 = row2’s distance to root2;
if (root1 == root2)
return distance1 – distance2;
else
return root1.RowIndex – root2.RowIndex;
end
How to Work-Around The Problem with Visual Studio 2008 SP1
With Visual Studio 2008 SP1, as a workaround, you could derive from the generated TableAdpaterManager and override its SortSelfReferenceRows method to implement this algorithm by yourself. Of course, you could put your code anywhere of your project, but it’s recommended to put them in the typed-dataset’s source code file. Right-click the typed-dataset (say, DataSet1.xsd), and choose “View Code”, Visual Studio will automatically create the source code file for you (DataSet1.cs in this case). It’s recommended you put your implementation code here.
Your code could be like this:
[C#]
// Derive from the generated TableAdapterManager and use it in your code instead
public class MyTableAdpaterManager : DataSet1TableAdapters.TableAdapterManager {
// Override this virtual method, and pass your own IComparer class
protected override void SortSelfReferenceRows(System.Data.DataRow[] rows, System.Data.DataRelation relation, bool childFirst) {
System.Array.Sort<System.Data.DataRow>(rows, new MySelfReferenceComparer(relation, childFirst));
}
private class MySelfReferenceComparer : object, global::System.Collections.Generic.IComparer<global::System.Data.DataRow> {
private global::System.Data.DataRelation _relation;
private int _childFirst;
internal MySelfReferenceComparer(global::System.Data.DataRelation relation, bool childFirst) {
this._relation = relation;
if (childFirst) {
this._childFirst = -1;
}
else {
this._childFirst = 1;
}
}
// Get the root of a row, and calculate its distance to the root
private global::System.Data.DataRow GetRoot(global::System.Data.DataRow row, out int distance) {
global::System.Diagnostics.Debug.Assert((row != null));
global::System.Data.DataRow root = row;
distance = 0;
// Save all traversed rows to avoid infinite loop
global::System.Collections.Generic.IDictionary<global::System.Data.DataRow, global::System.Data.DataRow> traversedRows = new global::System.Collections.Generic.Dictionary<global::System.Data.DataRow, global::System.Data.DataRow>();
traversedRows[row] = row;
global::System.Data.DataRow parent = row.GetParentRow(this._relation, global::System.Data.DataRowVersion.Default);
while ((parent != null) && !traversedRows.ContainsKey(parent)) {
distance++;
root = parent;
traversedRows[parent] = parent;
parent = parent.GetParentRow(this._relation, global::System.Data.DataRowVersion.Default);
}
// This is mainly for Deleted rows
if (distance == 0) {
traversedRows.Clear();
traversedRows[row] = row;
parent = row.GetParentRow(this._relation, global::System.Data.DataRowVersion.Original);
while ((parent != null) && !traversedRows.ContainsKey(parent)) {
distance++;
root = parent;
traversedRows[parent] = parent;
parent = parent.GetParentRow(this._relation, global::System.Data.DataRowVersion.Original);
}
}
return root;
}
public int Compare(global::System.Data.DataRow row1, global::System.Data.DataRow row2) {
if (object.ReferenceEquals(row1, row2)) {
return 0;
}
if ((row1 == null)) {
return -1;
}
if ((row2 == null)) {
return 1;
}
// Get root of row1 and calculate its distance to the root
int distance1 = 0;
global::System.Data.DataRow root1 = this.GetRoot(row1, out distance1);
// Get root of row2 and calculate its distance to the root
int distance2 = 0;
global::System.Data.DataRow root2 = this.GetRoot(row2, out distance2);
if (object.ReferenceEquals(root1, root2)) {
return this._childFirst * distance1.CompareTo(distance2);
}
else {
// Compare root1 and root2 with their row index to ensure the correct sort order
global::System.Diagnostics.Debug.Assert((root1.Table != null) && (root2.Table != null));
if (root1.Table.Rows.IndexOf(root1) < root2.Table.Rows.IndexOf(root2)) {
return -1;
}
else {
return 1;
}
}
}
}
}
[VB]
' Derive from the generated TableAdapterManager and use it in your code instead
Public Class MyTableAdapterManager
Inherits DataSet1TableAdapters.TableAdapterManager
' Override this virtual method, and pass your own IComparer class
Protected Overrides Sub SortSelfReferenceRows(ByVal rows() As System.Data.DataRow, ByVal relation As System.Data.DataRelation, ByVal childFirst As Boolean)
Global.System.Array.Sort(Of Global.System.Data.DataRow)(rows, New MySelfReferenceComparer(relation, childFirst))
End Sub
Private Class MySelfReferenceComparer
Inherits Object
Implements Global.System.Collections.Generic.IComparer(Of Global.System.Data.DataRow)
Private _relation As Global.System.Data.DataRelation
Private _childFirst As Integer
Friend Sub New(ByVal relation As Global.System.Data.DataRelation, ByVal childFirst As Boolean)
Me._relation = relation
If childFirst Then
Me._childFirst = -1
Else
Me._childFirst = 1
End If
End Sub
' Get root of a row, and calculate its distance to the root
Private Function GetRoot(ByVal row As Global.System.Data.DataRow, ByRef distance As Integer) As Global.System.Data.DataRow
Global.System.Diagnostics.Debug.Assert((Not (row) Is Nothing))
Dim root As Global.System.Data.DataRow = row
distance = 0
' Save all traversed rows to avoid infinite loop
Dim traversedRows As Global.System.Collections.Generic.IDictionary(Of Global.System.Data.DataRow, Global.System.Data.DataRow) = New Global.System.Collections.Generic.Dictionary(Of Global.System.Data.DataRow, Global.System.Data.DataRow)()
traversedRows(row) = row
Dim parent As Global.System.Data.DataRow = row.GetParentRow(Me._relation, Global.System.Data.DataRowVersion.[Default])
Do While ((Not (parent Is Nothing)) _
AndAlso (traversedRows.ContainsKey(parent) = False))
distance = distance + 1
root = parent
traversedRows(parent) = parent
parent = parent.GetParentRow(Me._relation, Global.System.Data.DataRowVersion.Default)
Loop
' This is mainly for Deleted rows
If (distance = 0) Then
parent = row.GetParentRow(Me._relation, Global.System.Data.DataRowVersion.Original)
Do While ((Not (parent Is Nothing)) _
AndAlso (traversedRows.ContainsKey(parent) = False))
distance = distance + 1
root = parent
traversedRows(parent) = parent
parent = parent.GetParentRow(Me._relation, Global.System.Data.DataRowVersion.Original)
Loop
End If
Return root
End Function
Public Function Compare(ByVal row1 As Global.System.Data.DataRow, ByVal row2 As Global.System.Data.DataRow) As Integer Implements Global.System.Collections.Generic.IComparer(Of Global.System.Data.DataRow).Compare
If Object.ReferenceEquals(row1, row2) Then
Return 0
End If
If (row1 Is Nothing) Then
Return -1
End If
If (row2 Is Nothing) Then
Return 1
End If
'Get root of row1 and calculate its distance to root
Dim distance1 As Integer = 0
Dim root1 As Global.System.Data.DataRow = Me.GetRoot(row1, distance1)
'Get root of row2 and calculate its distance to root
Dim distance2 As Integer = 0
Dim root2 As Global.System.Data.DataRow = Me.GetRoot(row2, distance2)
If Object.ReferenceEquals(root1, root2) Then
Return Me._childFirst * distance1.CompareTo(distance2)
Else
' Compare root1 and root2 with their row index to ensure the correct sort order
Global.System.Diagnostics.Debug.Assert((Not root1.Table Is Nothing) AndAlso (Not root2.Table Is Nothing))
If (root1.Table.Rows.IndexOf(root1) < root2.Table.Rows.IndexOf(root2)) Then
Return -1
Else
Return 1
End If
End If
End Function
End Class
End Class
For the complete sample code, you could download it at Code Gallery. Cheers!
-
DataRepeater control is one of the most important controls in Visual Basic PowerPacks. John Chen has recently posted a great blog about this control’s background and usage. As a complement, I want to talk about how to customize DataRepeater item appearance here.
Generally speaking, you could change appearance of DataRepeater items in approaches below:
1. To set appearance of all items
a. At design time
Select DataRepeater’s ItemTemplate, and set its appearance-related properties in Property Window, like the snapshot below shows. These properties will be copied and applied to all items at run time.
b. At run time
At run time, you could reset properties of DataRepeater’s ItemTemplate. New properties will overwrite corresponding properties you set at design time, and apply to all items, no matter whether the item has been scrolled into view or not.
2. To set appearance of an individual item
a. At design time
Not surprisingly, there is no way to do this. Because all items are dynamically created from ItemTemplate at run time, they do not exist at design time at all.
b. At run time
You could handle DrawItem event to change appearance of an individual item when it is scrolled into view.
In this article, I want to talk about how to set appearance of all items at run time, because users tend to forget to put their ItemTemplate-modifying code block between BeginResetItemTemplate and EndResetItemTemplate methods, which causes the appearance of DataRepeater items unpredictable.
What the code should be like
Suppose you have a DataRepeater control named dataRepeater1, and you want to modify its ItemTemplate properties at run time, then the code should be like:
[VB]
Me.DataRepeater1.BeginResetItemTemplate()
' Code to change appearance properties of ItemTemplate and its child controls
Me.DataRepeater1.ItemTemplate.BackColor = Color.Blue
Me.DataRepeater1.ItemTemplate.Controls("textBox1").Visible = False
......
Me.DataRepeater1.EndResetItemTemplate()
[C#]
this.dataRepeater1.BeginResetItemTemplate();
// Code to change appearance properties of ItemTemplate and its child controls
this.dataRepeater1.ItemTemplate.BackColor = Color.Blue;
this.dataRepeater1.ItemTemplate.Controls["textBox"].Visible = false;
......
this.dataRepeater1.EndResetItemTemplate();
Why need to call BeginResetItemTemplate and EndResetItemTemplate method
About the reason, I want to talk a little about the underlying implementation of DataRepeater. The implementation I’m going to talk about is current design of DataRepeater (also simplified to make it more understandable), and is possible to change in the future.
For performance reasons, DataRepeater does NOT create one DataRepeaterItem UI object for each data item. Instead, it just creates a few DataRepeaterItem instances (by cloning ItemTemplate), re-uses them, and creates some new ones on demand. These instances are stored in two Queues, let’s call them DisplayItemsQueue and SpareItemsQueue. DisplayItemsQueue holds real DataRepeaterItem objects for all visible data items, i.e. items that are located within visible area of DataRepeater control. When a new data item is scrolled into view, DataRepeater tries to retrieve a DataRepeaterItem object from SpareItemsQueue, binds it to the data item, raises DrawItem event for it so that user has a chance to customize its appearance, puts it to DisplayItemQueue and displays it; when a data item is scrolled out of view, its DataRepeateritem object is revoked, and is put into SpareItemsQueue for future re-use. When DataRepeater tries to retrieve a DataRepeateritem object from SpareItemsQueue and if this Queue is empty, a new DataRepeateritem object will be created from ItemTemplate and directly returned.
So, what happens when you change ItemTemplate’s properties? Take the VB code for example.
1 Without calling BeginResetItemTemplate and EndResetItemTemplate:
// Before updating ItemTemplate properties, DataRepeater has already created some DataRepeaterItem objects for visible items. These DataRepeaterItem objects’ properties will NOT be affected by code below.
Me.DataRepeater1.ItemTemplate.BackColor = Color.Blue
Me.DataRepeater1.ItemTemplate.Controls("textBox1").Visible = False
;
......
// The updated properties only apply to new DataRepeaterItem objects that are created on demand later.
2 When calling BeginResetItemTemplate and EndResetItemTemplate:
// When calling BeginResetItemTemplate, all already-created DataRepeaterItem objects are removed from the Queue and discarded.
Me.DataRepeater1.BeginResetItemTemplate()
Me.DataRepeater1.ItemTemplate.BackColor = Color.Blue
Me.DataRepeater1.ItemTemplate.Controls("textBox1").Visible = False
......
Me.DataRepeater1.EndResetItemTemplate()
// After calling EndResetItemTemplate(), all DataRepeaterItem objects are re-created from the updated ItemTemplate and the updated changes will be applied.
-
After you install Microsoft Visual Studio 2008 Service Pack 1, in some situations, when you try to use Data Source Configuration Wizard to create a connection, you may see the following error messages,
Error message 1:
Could not load type 'Microsoft.VisualStudio.DataDesign.SyncDesigner.SyncFacade.SyncManager' from assembly Microsoft.VisualStudio.DataDesign.SyncDesigner.DslPackage, Version = 9.0.0.0, Culture=neutral, PublicKey Token=b03f5f7f11d50a3a
Error message 2:
The type initializer for 'Microsoft.VisusalStudio.DataDesign.SyncDesigner.SyncFacade.SyncTableConfigManager' threw an exception
These are due to an incomplete installation of Service Pack 1 or an incorrect installation of Microsoft Synchronization Services for ADO.net.
We have written a KB article (KB2001115) to explain the details and provide the solutions. Please see here if you met these issues,
http://support.microsoft.com/default.aspx/kb/2001115
If you need any further help, let us know.
-
The DataRepeater control is available in the Visual Basic Power Packs.
As Yun Feng introduced in his post, the Visual Basic Power Packs is included in Visual Studio 2008 Service Pack 1 and will also be shipped in Visual Studio 2010.
The DataRepeater control is my favorite component in the Visual Basic Power Packs, which I proudly participated in from start to finish. Recently I monitored the Visual Basic Power Packs forum and I found that customers are saying nice things about the DataRepeater control as well. A lot of good questions have been asked in the forum. For example, how to print all the data in the DataRepeater, how to display unbound data, how to host a data bound ComboBox in the DataRepeater, etc… I feel motivated to write some posts to answer these non-obvious questions.
In this post, I will first present the unique features of the DataRepeater control from my perspective. Then I will show a simple walk through for using a DataRepeater control in a data binding application. At the end, I will list the resources available for reference.
First of all, I would like to point out that the DataRepeater control is a true .Net Windows Forms control. It can be used not only by Visual Basic but also for any other .Net language (e.g. C#). You may be curious why it is under the Visual Basic Power Packs “brand”. The project was initially designed to help VB6 customers migrating to the .Net world with familiar experience and environment. We started the project with the Visual Basic customers in mind as there was already a Data Repeater Control in Visual Basic 6 and unfortunately there was no corresponding control in Visual Studio.Net 2002 to 2008. After we created the control we realized that it would be good for all .NET customers and we decided to make it available to all of Visual Studio.
To differentiate the DataRepeater controls in VB6 and the one now in Visual Studio 2008 SP1, in this article I would call them VB6 DataRepeater and PowerPacks DataRepeater (or simply DataRepeater). The PowerPacks DataRepeater inherits the basic concept of the VB6 DataRepeater control to display repeated data in a scrollable container. Even more, the PowerPacks DataRepeater has significant improvements over the VB6 DataRepeater. The PowerPacks DataRepeater is modern (as I said, it is a .Net control can be used by all languages), more powerful, has more functions, and yet easier to use taking all the advantages of the .Net and Visual Studio Data Access tools and technologies.
For example, in VB6, you need to create an intermediate user control to host the VB6 DataRepeater and set up the data binding there, build it and then include the user control in the host form. With PowerPacks DataRepeater, the intermediate user control is no longer needed! You can now drag and drop the DataRepeater control onto the hosting form directly, and then drag and drop controls on its item template section. You will enjoy a truly WYSIWYG design experience.
Although both DataRepeater controls can host free style controls in the item template section, the PowerPacks DataRepeater allows you to have even more controls on the layout and the appearance of the repeating items. You can use the DrawItem event to customize the color or other appearances as you wish. The LayoutStyle property lets you specify orientation (vertical or horizontal) of the displayed items. The orientation feature is really attractive and I have seen some customers choose the DataRepeater over the DataGridView simply because the DataRepeter can show data in horizontal orientation.
Talking about the DataGridView, the PowerPacks DataRepeater control can be seen as an alternative control to the DataGridView. The PowerPacks DataRepeater follows the design of the DataGridView and you can find many properties (e.g. the Virtual Mode property) in DataRepeater control in parity with the DataGridView. This description from MSDN document best describes the role of the PowerPacks DataRepeater -- “It can be used as an alternative to the DataGridView control when you need more control over the layout of the data.”
The PowerPacks DataRepeater control applies a virtualization technique to display the data in a fashion that scrolling and navigation is fast while rich UI and visual effects are still retained. For example, all displayed controls on the display surface are real controls and you can interact with them without delay. Scrolling is smoothly simulated while VB6 DataRepeater does not have a scroll effect.
You can further improve performance by turning on the Virtual Mode when you want to display large quantities of tabular data in a PowerPacks DataRepeater control. The Virtual Mode allows you explicitly manage the control’s interaction with its data source. Therefore you can load the data rows only when necessary to reduce the initial loading time. You can also reduce the memory consumption by only loading part of the rows into memory at any given time. Notice that if you already have all data rows loaded into memory like a DataSet filled by the TableAdapter.Fill method, you will not have the performance gain with Virtual Mode.
To demonstrate the ease of use of the PowerPacks DataRepeater control, let me give you a walkthrough for building an application with the DataRepeater control.
First I would create a C# Windows Forms Application in Visual Studio 2008 SP1 or 2010 (yes, I purposely choose a C# project here to show that it is not only for VB, the steps in this demo have no difference between C# and VB).
And then I want to create a DataSet data source with the DataSource Configuration Wizard or DataSet Designer. I will assume you are familiar with this part and a DataSet with Employees table connecting to the sample Northwind database is ready to use. You will see that the Data Sources Window on the left hand side is populated with the Employees Datatable (see Picture 1 bellow).
Picture 1 – DataSet designer and Data Sources window.
Now, let’s open the Form and show the Toolbox. You can find the Power Packs tab (see picture 2, marker 1, shortened as Pic.2-1), expand it and you will find the DataRepeater toolbox item (Pic.2-2). Drag and drop the DataRepeater toolbox item to the form (Pic. 2-3), you will see two sections in the location of the DataRepeater1 control on the form. The outer section (Pic.2-4) is the total display area of the control and the inner section (Pic.2-5, a.k.s. the Item Template section) holds the repeating items. It also defines the size of the repeating items and the location of the first item. You can fine tune the look and feel of the outer and inner section of the DataRepeater control as you wish.
Picture 2. Drag and Drop DataRepeater to the Form
I select my DataRepeater by clicking on the outer section of the DataRepeater, the Property Brower should show as in Pic. 3-1. And I set the anchor as top-left-right (Pic.3-2) and the LayoutStyle as Horizontal (Pic. 3-3).
Picture 3. DataRepeater layout
Now, let’s fill the DataRepeater item template section with some data bound controls. I show the Data Sources Window (Pic. 4-1), change the employees table’s view to detail (Pic. 4-2), change the First and Last Name to label (Pic. 4-3), leave the Title as default TextBox (Pic. 4-4), and change the Photo to a PictureBox (Pic. 4-5). I then drag and drop these items to the DataRepeater item template section as indicated in the picture and remove the unnecessary extra labels. Note that it will also cause the auto creation of the navigator bar (Pic. 4-6), Dataset, BindingSource, TableAdapter, etc.(Pic. 4-7). The experience is exactly the same as when you drag and drop detail view items from the Data Sources window to a form or any container control. The difference is now the data bound items in the Item Template section will now be repeated at run time.
Picture 4. Drag and drop detail view items from Data Sources window to the item template section
Now I am happy with the setup and eager to see what is out there, I hit F5 to test my app, and I got this result, that is all I need. You can scroll the data by clicking on the scroll bar or the forward and backward button on the navigation bar. You can select an item, and modify the value on any editable control on the item like Title text box (Pic. 5-1). You can add new item (Pic. 5-2), delete an item (Pic. 5-3). And you can save all the changes (Add/Delete/Update) with the save button (Pic. 5-4).
Picture 5. Run time result
To summarize, creating a data binding application can be simply done with these 3 steps: 1) Create the data source; 2) Drag and drop a DataRepeater control to the form and adjust its look and feel; 3) Drag and drop detail view items from Data Sources Window to the item template section of the DataRepeater control.
Lastly, let me wrap up with some more information:
As mentioned above, my colleagues and I will post more articles to answer some advanced or non-obvious How-Tos in the near future. Stay tuned!
----------------------------------------------------------------------------
Follow-up blogs:
- 8/27/09 Gavin Fu: How to Change Appearance of all DataRepeater Items at Run Time
- 9/8/09 John Chen Writing Event Handler for controls in a DataRepeaterItem
-
Some time ago, we built a navigation bar for our WPF application to navigate data. We can make it even more powerful, and add CRUD support into it. Because we are using Entity Data Framework, it’s pretty easy to achieve this. Don’t believe it? Let’s first add the following buttons into our navigation bar we built late time:
<StackPanel Height="28" HorizontalAlignment="Left" Margin="103,232,0,0" Name="stackPanel2" VerticalAlignment="Top" Width="119" Orientation="Horizontal">
<Button Background="BlanchedAlmond" Content="Insert" FontWeight="Bold" Height="23" Name="insertButton" Width="Auto" Click="insertButton_Click" />
<Button Background="BlanchedAlmond" Content="Save" FontWeight="Bold" Height="23" Name="saveButton" Width="Auto" Click="saveButton_Click"/>
<Button Background="BlanchedAlmond" Content="Delete" FontWeight="Bold" Height="23" Name="deleteButton" Width="Auto" Click="deleteButton_Click" />
</StackPanel>
It’s something like below:
Now let’s add the following event handler methods for each button:
1. Insert button. Insert button adds a new empty entry for users to fill in data.
What we do here is pretty simple. We leverage the method IEditableCollectionViewSource.AddNew() to create a new entry into the current CollectionView. More info can be found here. Because the EmployeeID is assigned in the table automatically when a new row is inserted, we don’t need users to fill in this item.
The code looks like below:
C#:
private void insertButton_Click(object sender, RoutedEventArgs e)
{
((BindingListCollectionView)(employeesViewSource__NorthwindEntities.View)).AddNew();
employeeIDTextBox.Text = "{Auto-assigned}";
}
VB:
Private Sub insertButton_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
CType(EmployeesViewSource__NorthwindEntities.View, BindingListCollectionView).AddNew()
employeeIDTextBox.Text = "{Auto-assigned}"
End Sub
2. Save button. Save button saves the current data into database.
Let’s use ObjectContext.SaveChanges() to persist all updates to the database. SaveChanges method has an overload version SaveChanges(Boolean). If Boolean is set to false, then you have to call AccetpAllChanges() after calling SaveChanges(Boolean). For more information about this method, please check here.
Here we also need to consider about concurrency errors. For more information about how to handle this, please see here.
The code looks like below:
C#:
private void saveButton_Click(object sender, RoutedEventArgs e)
{
try
{
northwindEntities.SaveChanges();
// Update the number of rows in the table in case it's updated by data insertion.
UpdateNavigatorUI();
}
catch (DBConcurrencyException ex)
{
// Add business logic code to resolve the concurrency violation...
}
}
VB:
Private Sub saveButton_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
Try
NorthwindEntities.SaveChanges()
' Update the number of rows in the table in case it's updated by data insertion.
UpdateNavigatorUI()
Catch ex As System.Data.DBConcurrencyException
' Add business logic code to resolve the concurrency violation...
End Try
End Sub
3. Delete button. Delete button tends to delete the current entry.
Here we instantiate an Employee object with the current item in the form and then use DeleteObject(TEntity) to do the deletion. However, the job isn’t submitted until users click Save button.
C#:
private void deleteButton_Click(object sender, RoutedEventArgs e)
{
Employee employee = employeesViewSource__NorthwindEntities.View.CurrentItem as Employee;
if (employee != null)
{
northwindEntities.DeleteObject(employee);
}
// Update the row count number and the current row number in the navigation bar.
UpdateNavigatorUI();
}
VB:
Private Sub deleteButton_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
Dim employee As Employee = CType(EmployeesViewSource__NorthwindEntities.View.CurrentItem, Employee)
If Not (employee Is Nothing) Then
NorthwindEntities.DeleteObject(employee)
End If
' Update the row count number and the current row number in the navigation bar
UpdateNavigatorUI()
End Sub
Done! Now we can hit F5 and play with it. Have fun!
-
Most of the time, your business application needs to deal with data stored in a database. Sometimes the database is on a remote server, while sometimes it is a local database file (SQL Server Compact database file, SQL Server Express database file, or Microsoft Access database file).
One of the frequently asked questions about debugging an application that has a local database file is: “my update method executes successfully, why the database data is not updated?”
My data is not updated!
For example, my project has a SQL Server Compact database file (Northwind.sdf), and I add a dataset with Customer table data in my database file:

In Program.cs file, input following code to Main method:
NorthwindDataSet ds = new NorthwindDataSet();
NorthwindDataSetTableAdapters.CustomersTableAdapter ta = new ConsoleApplication1.NorthwindDataSetTableAdapters.CustomersTableAdapter();
ta.Fill(ds.Customers);
ds.Customers[0].Postal_Code = "11111";
int result = ta.Update(ds);
Now if you hit F5 to debug this project and check the return value of Update method, the value is 1, which means one row was successfully updated. Now double click the Northwind.sdf file in Solution Explorer, and choose “Show Table Data” on Customers table:
You will notice that the Postal_Code is not updated at all!
What happened?
Let’s check the property of the local database file:

There’s a property “Copy to Output Directory” and the default value is “Copy if newer” (if you’re using .mdf or .mdb file, the default value is “Copy always”). You could check this MSDN document to learn what this property means. In short, the local database file will be copied to Output directory, and THAT database is the one that will get updated.
Let’s select “Show All Files” button on upper left corner of Solution Explorer, and navigate to bin/Debug folder. Now you could see the Northwind.sdf file in output directory:

If you double click on this file, Server Explorer will create a connection to this database file. “Show Table Data” now! You will see the Postal_Code of first customer is now "11111”.
What other options do I have?
If you don’t want Visual Studio to copy the database file for you, you could set the “Copy to Output Directory” property to “Do not copy”. Then it’s your choice when and how to overwrite the database file. Of course, you still need two copies of database file: at design time, you’re using the database file in solution directory, while at run time, you’re modifying the one in output directory.
If you want to get rid of this “two-copy-confusion” and want both design time and run time using the same copy of database file, you should select No when IDE offers to “copy the file to your project and modify the connection”.
One thing worth mentioning is, if you leave the database file outside of your project, the path of the database file will actually be hardcoded into your project. Before deployment, please update this setting and replace the full path with a relative path.
Cheers!
08/04 Update: this MSDN How to is helpful too. It describes in detail how to manage local data files in your project.