Silverlight: Product Maintenance Application ( Part 4 – Adding some controls )

Following up on this post I thought the first control I’d build would be a graph control so I just referenced the Silverlight Toolkit for its graphs and very quickly had;

<UserControl
    x:Class="SilverlightApplication6.Controls.ChartDisplayControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:charts="clr-namespace:System.Windows.Controls.DataVisualization.Charting;assembly=System.Windows.Controls.DataVisualization.Toolkit"
    xmlns:localCtl="clr-namespace:SilverlightApplication6.Controls">
    <Grid
        x:Name="LayoutRoot">
        <Grid.RowDefinitions>
            <RowDefinition
                Height="Auto" />
            <RowDefinition
                Height="*" />
        </Grid.RowDefinitions>
        <StackPanel
            Orientation="Horizontal">
            <TextBlock
                Text="Historical sales data for product" />
            <TextBlock
                Margin="5,0,0,0"
                Text="{Binding ProductName}" />
        </StackPanel>
        <charts:Chart
            Title="{Binding ProductName}"
            x:Name="salesChart"
            Grid.Row="1">
            <charts:Chart.Series>
                <charts:AreaSeries
                    Title="{Binding ProductName}"
                    ItemsSource="{Binding Path=ProductSales}"
                    IndependentValueBinding="{Binding Path=MonthYearLabel}"
                    DependentValueBinding="{Binding Path=TotalSales}" />
            </charts:Chart.Series>
        </charts:Chart>
    </Grid>
</UserControl>

there’s no code behind this control – it just displays a graph with a title and my DataModel.ChartData property provides everything that it needs and so it’s entirely data-bound. In fact, it’s only there as an additional control to add a couple of lines of text. Seems easy but I’ve no idea whether I’ll get it to work in WPF at a later point as the WPF Charts aren’t in place just yet although there’s a version that is available to experiment with and I’m hoping that’ll see me through 🙂

So, that’s my Chart control. My search control is even simpler;

<UserControl x:Class="SilverlightApplication6.SearchControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:SilverlightApplication6">
    <Grid x:Name="LayoutRoot">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition
                Width="Auto" />
        </Grid.ColumnDefinitions>
        <TextBlock
            Text="Search Term:"
            Margin="5"/>
        <TextBox
            IsEnabled="{Binding Source={StaticResource roleStatus},Path=IsUser,Mode=TwoWay}"
            Grid.Column="1"
            HorizontalAlignment="Stretch"
            x:Name="txtSearchTerm" />
        <Button
            IsEnabled="{Binding Source={StaticResource roleStatus},Path=IsUser,Mode=TwoWay}"
            Click="OnSearch"
            Grid.Column="2"
            Content="Search"
            Margin="5" />
    </Grid>
</UserControl>

with a bit of code behind it to fire an event whenever the search term changes ( this could have been data bound back into my model and then these kind of events would go away but ( for some reason ) I seem to quite like them as I see them as a form of commands );

 public class SearchTermEventArgs : EventArgs  {    public string SearchTerm { get; set; }  }  public partial class SearchControl : UserControl  {    public event EventHandler<SearchTermEventArgs> SearchTermChanged;    public SearchControl()    {      InitializeComponent();    }    void OnSearch(object sender, EventArgs args)    {      if (!string.IsNullOrEmpty(txtSearchTerm.Text) &&         (SearchTermChanged != null))      {        SearchTermChanged(this,           new SearchTermEventArgs() { SearchTerm = txtSearchTerm.Text });      }    }  }

so, that’s my search control and I have code somewhere to handle the event that it fires and update my DataModel.

Then there’s my ProductsDisplayGrid control which looks something like what’s below in that it’s basically a DataGrid with custom columns. The DataGrid enables/disables itself based on role and is read-only or not based on role. There are a couple of “WpfComboBoxes” ( see previous post ) that use my ReferenceData to display lists of Suppliers and Categories and there’s a button ( available depending on role ) which allows you to view the chart of historical sales data for a particular product.

Finally, there’s a panel which displays paging controls along with a button to do insert and another to save changes. There’s an odd hack in here with a rectangle right now which I’ll remove later on…

<UserControl

    x:Class="SilverlightApplication6.Controls.ProductsDisplayGridControl"

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:local="clr-namespace:SilverlightApplication6"

    xmlns:localCtl="clr-namespace:SilverlightApplication6.Controls"

    xmlns:df="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data">

    <UserControl.Resources>

        <!– This is a hack to let me get to my reference data, struggled with

             binding to it otherwise –>

        <Rectangle

            x:Name="referenceDataSource" />

    </UserControl.Resources>

    <localCtl:BusyDisplayControl

        x:Name="busyControl" IsBusy="{Binding ProductData.IsLoading}">

        <localCtl:BusyDisplayControl.Content>

            <Grid>

                <Grid.RowDefinitions>

                    <RowDefinition

                        Height="*" />

                    <RowDefinition

                        Height="Auto" />

                </Grid.RowDefinitions>

                <df:DataGrid

                    IsEnabled="{Binding Source={StaticResource roleStatus},Path=IsUser}"

                    IsReadOnly="{Binding Source={StaticResource roleStatus},Path=IsEditor,Converter={StaticResource booleanInverter}}"

                    Margin="10"

                    x:Name="myGrid"

                    AutoGenerateColumns="False"

                    ItemsSource="{Binding ProductData.Products,Mode=TwoWay}">

                    <df:DataGrid.Columns>

                        <df:DataGridTextColumn

                            Binding="{Binding ProductName}"

                            Header="Name" />

                        <df:DataGridCheckBoxColumn

                            Binding="{Binding Discontinued}"

                            Header="Discontinued" />

                        <df:DataGridTextColumn

                            Binding="{Binding UnitPrice}"

                            Header="Unit Price" />

                        <df:DataGridTextColumn

                            Binding="{Binding UnitsInStock}"

                            Header="Stocked Units" />

                        <df:DataGridTextColumn

                            Binding="{Binding UnitsOnOrder}"

                            Header="Units on Order" />

                        <df:DataGridTextColumn

                            Binding="{Binding QuantityPerUnit}"

                            Header="Quantity/Unit" />

                        <df:DataGridTextColumn

                            Binding="{Binding ReorderLevel}"

                            Header="Reorder Level" />

                        <df:DataGridTemplateColumn

                            Header="Category">

                            <df:DataGridTemplateColumn.CellTemplate>

                                <DataTemplate>

                                    <local:WpfComboBox

                                        IsEnabled="{Binding Source={StaticResource roleStatus},Path=IsEditor}"

                                        ItemsSource="{Binding Source={StaticResource referenceDataSource},Path=DataContext.ReferenceData.Categories}"

                                        DisplayMemberPath="Title"

                                        SelectedValuePath="Id"

                                        SelectedValue="{Binding CategoryID,Mode=TwoWay}" />

                                </DataTemplate>

                            </df:DataGridTemplateColumn.CellTemplate>

                            <df:DataGridTemplateColumn.CellEditingTemplate>

                                <DataTemplate>

                                    <local:WpfComboBox                                       
                                        ItemsSource="{Binding Source={StaticResource referenceDataSource},Path=DataContext.ReferenceData.Categories}"

                                        DisplayMemberPath="Title"

                                        SelectedValuePath="Id"

                                        SelectedValue="{Binding CategoryID,Mode=TwoWay}" />

                                </DataTemplate>

                            </df:DataGridTemplateColumn.CellEditingTemplate>

                        </df:DataGridTemplateColumn>

                        <df:DataGridTemplateColumn

                            Header="Supplier">

                            <df:DataGridTemplateColumn.CellTemplate>

                                <DataTemplate>

                                    <local:WpfComboBox

                                        IsEnabled="{Binding Source={StaticResource roleStatus},Path=IsEditor}"

                                        ItemsSource="{Binding Source={StaticResource referenceDataSource},Path=DataContext.ReferenceData.Suppliers}"

                                        DisplayMemberPath="Title"

                                        SelectedValuePath="Id"

                                        SelectedValue="{Binding SupplierID,Mode=TwoWay}" />

                                </DataTemplate>

                            </df:DataGridTemplateColumn.CellTemplate>

                            <df:DataGridTemplateColumn.CellEditingTemplate>

                                <DataTemplate>

                                    <local:WpfComboBox

                                        ItemsSource="{Binding Source={StaticResource referenceDataSource},Path=DataContext.ReferenceData.Suppliers}"

                                        DisplayMemberPath="Title"

                                        SelectedValuePath="Id"

                                        SelectedValue="{Binding SupplierID,Mode=TwoWay}" />

                                </DataTemplate>

                            </df:DataGridTemplateColumn.CellEditingTemplate>

                        </df:DataGridTemplateColumn>

                        <df:DataGridTemplateColumn>

                            <df:DataGridTemplateColumn.CellTemplate>

                                <DataTemplate>

                                    <Button

                                        IsEnabled="{Binding Source={StaticResource roleStatus},Path=IsViewer}"

                                        Content="View"

                                        Click="OnViewProductSalesChart" />

                                </DataTemplate>

                            </df:DataGridTemplateColumn.CellTemplate>

                        </df:DataGridTemplateColumn>

                    </df:DataGrid.Columns>

                </df:DataGrid>

                <StackPanel

                    Orientation="Horizontal"

                    Grid.Row="1"

                    Margin="10">

                    <Button

                        Content="Insert"

                        Click="OnInsert"

                        IsEnabled="{Binding Source={StaticResource roleStatus},Path=IsEditor}" />

                    <Button

                        Content="Save Changes"

                        Click="OnSave"

                        IsEnabled="{Binding ProductData.HasChangedProducts}" />

                </StackPanel>

                <StackPanel

                    x:Name="stackPanelNavigation"

                    Visibility="{Binding ProductData.HasData,Converter={StaticResource converter}}"

                    Orientation="Horizontal"

                    Grid.Row="1"

                    HorizontalAlignment="Right"

                    Margin="10">

                    <TextBlock

                        VerticalAlignment="Center"

                        Text="{Binding ProductData.SearchResultDetails.Page}"

                        Margin="2" />

                    <TextBlock

                        VerticalAlignment="Center"

                        Text="of"

                        Margin="2" />

                    <TextBlock

                        VerticalAlignment="Center"

                        Text="{Binding ProductData.SearchResultDetails.TotalPages}"

                        Margin="2" />

                    <Button

                        Content="Previous"

                        IsEnabled="{Binding ProductData.SearchResultDetails.HasPrevPage}"

                        Click="OnPrevious"

                        Margin="2" />

                    <Button

                        Content="Next"

                        IsEnabled="{Binding ProductData.SearchResultDetails.HasNextPage}"

                        Click="OnNext"

                        Margin="2" />

                </StackPanel>

            </Grid>

        </localCtl:BusyDisplayControl.Content>

        <localCtl:BusyDisplayControl.BusyContent>

            <TextBlock

                HorizontalAlignment="Center"

                VerticalAlignment="Center"

                Text="Working…" />

        </localCtl:BusyDisplayControl.BusyContent>

    </localCtl:BusyDisplayControl>

</UserControl>

Code behind this is pretty minimal in that it just takes the various button presses and turns them into events and so this fragment of code probably tells all of the story here;

 public enum PageDirection  {    Next,    Previous  }  public class PageNavigationEventArgs : EventArgs  {    public PageDirection Direction { get; set; }  }  public class ViewProductEventArgs : EventArgs  {    public int ProductId { get; set; }    public string ProductName { get; set; }  }  public partial class ProductsDisplayGridControl : UserControl  {    public event EventHandler<ViewProductEventArgs> ViewProduct;    public event EventHandler<PageNavigationEventArgs> PageNavigation;    public event EventHandler SaveRequested;    public event EventHandler InsertRequested;

I’ve omitted the rest – it’s just handlers that fire events.

All these controls come together on my main XAML page for the application and the code that lives behind that page takes on a controlling role in that it handles events and wires them onto the DataModel. Here’s the code from behind that page;

  public partial class MainPage : UserControl  {    public MainPage()    {      InitializeComponent();      dataModel = new DataModel();      this.DataContext = dataModel;    }    void OnLoggedIn(object sender, EventArgs e)    {      dataModel.ReferenceData.LoadFailed += OnFailLoadReferenceData;      dataModel.ReferenceData.LoadAsync();      dataModel.ProductData.LoadFailed += OnFailedLoadProductData;      dataModel.ProductData.SaveCompleted += OnProductSaveCompleted;      dataModel.SalesChartData.LoadFailed += OnFailedLoadChartData;    }    void OnProductSaveCompleted(object sender, ProductSavedEventArgs e)    {      if (e.RecordsFailed != 0)      {        MessageBox.Show(string.Format("Failed to save {0} records out of {1}. Refresh data and try again",          e.RecordsFailed, e.RecordsAttempted));      }    }    void OnSearchTermChanged(object sender, SearchTermEventArgs e)    {      dataModel.ProductData.SearchTerm.SearchTerm = e.SearchTerm;      dataModel.ProductData.LoadAsync();    }    void OnViewProduct(object sender, ViewProductEventArgs args)    {      dataModel.SalesChartData.ProductId = args.ProductId;      dataModel.SalesChartData.ProductName = args.ProductName;      dataModel.SalesChartData.LoadAsync();    }    void OnFailedLoadChartData(object sender, AsyncCompletedEventArgs e)    {      MessageBox.Show("Failed to load chart data");    }    void OnFailedLoadProductData(object sender, AsyncCompletedEventArgs e)    {      MessageBox.Show("Failed to load product data");    }    void OnFailLoadReferenceData(object sender, AsyncCompletedEventArgs e)    {      MessageBox.Show("Failed to load reference data for lookups");    }    private void OnLoginFailed(object sender, EventArgs e)    {      MessageBox.Show("Sorry, login failed");    }    private void OnGridPageNavigation(object sender, PageNavigationEventArgs e)    {      switch (e.Direction)      {        case PageDirection.Next:          dataModel.ProductData.NextPage();          break;        case PageDirection.Previous:          dataModel.ProductData.PreviousPage();          break;        default:          break;      }    }    private void OnProductGridSaveRequested(object sender, EventArgs e)    {      dataModel.ProductData.SaveChangesAsync();    }     private void OnProductGridInsertRequested(object sender, EventArgs e)    {      dataModel.ProductData.AddProduct();    }    DataModel dataModel;  }

and so it creates the model, handles events from controls and then passes the necessary actions to the DataModel.

With that all in place, I have a working app ( up to a point anyway ) and so in the next post I’ll run it, post the source-code and start to think about porting it to WPF…