Search code examples
c#.netwpfwcfn-tier-architecture

Displaying Database Data in a Datagrid using WPF and WCF in an N-Tier Application


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());
        }
    }
}

Solution

  • 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>
    

    Remarks

    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.