Having completed the following tutorial: https://learn.microsoft.com/en-us/visualstudio/data-tools/walkthrough-creating-an-n-tier-data-application?view=vs-2019
I'm trying to make the same tutorial work with WPF instead of Windows Forms but can't for the life of me get it to display the database data in the Datagrid. My code for everything except the PresentationTier is the same (but with my own database data). My .xaml code is (largely default from dragging the table from the Data Sources tab):
<Page
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:DataEntityTier="clr-namespace:DataEntityTier;assembly=DataEntityTier"
x:Class="PresentationTier.ViewProducts"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
Title="Worker-ViewProducts" Background="White" Loaded="Page_Loaded">
<Page.Resources>
<DataEntityTier:WMSDataSet x:Key="wMSDataSet"/>
<CollectionViewSource x:Key="productsViewSource" Source="{Binding products, Source={StaticResource wMSDataSet}}"/>
<CollectionViewSource x:Key="warehousesViewSource" Source="{Binding warehouses, Source={StaticResource wMSDataSet}}"/>
</Page.Resources>
<Grid Margin="10" DataContext="{StaticResource productsViewSource}">
<DataGrid x:Name="productsDataGrid" AutoGenerateColumns="False" EnableRowVirtualization="True" ItemsSource="{Binding Source={StaticResource productsViewSource}}" Margin="165,230,215,0" RowDetailsVisibilityMode="VisibleWhenSelected">
<DataGrid.Columns>
<DataGridTextColumn x:Name="product_idColumn" Binding="{Binding product_id}" Header="product id" IsReadOnly="True" Width="SizeToHeader"/>
<DataGridTextColumn x:Name="account_idColumn" Binding="{Binding account_id}" Header="account id" Width="SizeToHeader"/>
<DataGridTextColumn x:Name="titleColumn" Binding="{Binding title}" Header="title" Width="SizeToHeader"/>
<DataGridTextColumn x:Name="skuColumn" Binding="{Binding sku}" Header="sku" Width="SizeToHeader"/>
</DataGrid.Columns>
</DataGrid>
<DataGrid x:Name="warehousesDataGrid" AutoGenerateColumns="False" EnableRowVirtualization="True" ItemsSource="{Binding Source={StaticResource warehousesViewSource}}" Margin="165,0,215,230" RowDetailsVisibilityMode="VisibleWhenSelected">
<DataGrid.Columns>
<DataGridTextColumn x:Name="warehouse_idColumn" Binding="{Binding warehouse_id}" Header="warehouse id" IsReadOnly="True" Width="SizeToHeader"/>
<DataGridTextColumn x:Name="nameColumn" Binding="{Binding name}" Header="name" Width="SizeToHeader"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Page>
and my xaml.cs code is:
using DataEntityTier;
using System.Windows;
using System.Windows.Controls;
namespace PresentationTier
{
public partial class ViewProducts : Page
{
WMSDataSet wMSDataSet;
public ViewProducts()
{
InitializeComponent();
Loaded += Page_Loaded;
}
private void Page_Loaded(object sender, RoutedEventArgs e)
{
wMSDataSet = new WMSDataSet();
ServiceReference1.Service1Client DataSvc = new ServiceReference1.Service1Client();
wMSDataSet.products.Merge(DataSvc.GetProducts());
wMSDataSet.warehouses.Merge(DataSvc.GetWarehouses());
}
}
}
You are operating on the wrong WMSDataSet
instance.
Your data bindings reference the instance defined in the page's ResourceDictionary
. But you are actually initializing a second instance in you Loaded
event handler.
The solution is to either instantiate the WMSDataSet
in code-behind and then add it to the ResourceDictionary
(or assign it to a bindable property) or to retrieve the XAML instance and initialize it.
The second solution would require the WMSDataSet
object to implement INotifyPropertyChanged
in order to make the data binding notice property changes in order to update the binding target. I recommend to expose the WMSDataSet
via a DependencyProperty
:
ViewProducts.xaml.cs
public partial class ViewProducts : Page
{
public static readonly DependencyProperty WmsDataSourceProperty = DependencyProperty.Register(
"WmsDataSource",
typeof(WMSDataSet),
typeof(ViewProducts),
new PropertyMetadata(default(WMSDataSet)));
public WMSDataSet WmsDataSource
{
get => (WMSDataSet) GetValue(ViewProducts.WmsDataSourceProperty);
set => SetValue(ViewProducts.WmsDataSourceProperty, value);
}
public ViewProducts()
{
InitializeComponent();
Loaded += Page_Loaded;
}
private void Page_Loaded(object sender, RoutedEventArgs e)
{
wmsDataSet = new WMSDataSet();
ServiceReference1.Service1Client dataSvc = new ServiceReference1.Service1Client();
wmsDataSet.products.Merge(dataSvc.GetProducts());
wmsDataSet.warehouses.Merge(dataSvc.GetWarehouses());
this.WmsDataSource = wmsDataSet;
}
}
ViewProducts.xaml
<Page>
<Page.Resources>
<CollectionViewSource x:Key="ProductsViewSource" Source="{Binding RelativeSource={RelativeSource AncestorType=local:ViewProducts}, Path=WmsDataSource.products}" />
<CollectionViewSource x:Key="WarehousesViewSource" Source="{Binding RelativeSource={RelativeSource AncestorType=local:ViewProducts}, Path=WmsDataSource.warehouses}" />
</Page.Resources>
<!-- When you set the DataContext to 'ViewProducts.WmsDataSource', you can directly bind to it
using {Binding} (without specifying the Binding.Source) -->
<Grid DataContext="{Binding RelativeSource={RelativeSource AncestorType=local:ViewProducts}, Path=WmsDataSource}">
<DataGrid ItemsSource="{Binding products}">
</DataGrid>
<DataGrid ItemsSource="{Binding warehouses}">
</DataGrid>
</Grid>
</Page>
There are some mistakes in your code. You doing a lot of redundant stuff, because you are doing it twice, where the latter overwrites the former declaration, introduces errors or unexpected behavior (e.g. blank controls):
Of course the already mentioned usage of two instances of WMSDataSet
.
Instantiated in XAML:
<DataEntityTier:WMSDataSet x:Key="wMSDataSet"/>
and code-behind:
this.wMSDataSet = new WMSDataSet();
Then you are setting the DataContext
of the parent element just to ignore it in your binding expressions:
<Grid DataContext="{StaticResource productsViewSource}">
<DataGrid ItemsSource="{Binding Source={StaticResource productsViewSource}}">
instead of:
<Grid DataContext="{StaticResource productsViewSource}">
<DataGrid ItemsSource="{Binding}">
Since the contents of hte common parent Grid
(the two DataGrid
elements) don't share the same data context, it is more confusing then helpful to set their DataContext
to a common source. Rather choose to define the following version:
<Grid>
<DataGrid ItemsSource="{Binding Source={StaticResource productsViewSource}}" />
<DataGrid ItemsSource="{Binding Source={StaticResource warehousesViewSource}}" />
</Grid>
or in the refactored version of my solution:
<Grid DataContext="{Binding RelativeSource={RelativeSource AncestorType=local:ViewProducts}, Path=WmsDataSource}">
<DataGrid ItemsSource="{Binding products}" />
<DataGrid ItemsSource="{Binding warehouses}" />
</Grid>
You are also subscribing to the Loaded
event twice, which leads to the event handler being called twice too. You first subscribe to PageLoaded
in XAML:
<Page Loaded="Page_Loaded">
and in code-behind:
public ViewProducts()
{
InitializeComponent();
Loaded += Page_Loaded;
}
Choose only one.