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…