I'm trying to implement a TreeView in WPF to show children that display the charge activity, adjustment activity, etc on a daily account balance. I was going to start by first rendering the daily account balance details as the parent node (which I've done successfully), and then try, for instance, showing a text segment as the child for testing and then expanding from there to show children that I would preferably render as tables to show what was charged that day, and then separately what was adjusted on a charge that day, etc. This is where I'm stuck.
Here's my XAML code:
<Window x:Class="Client_Invoice_Auditor.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Client_Invoice_Auditor"
xmlns:self="clr-namespace:Client_Invoice_Auditor.CoreClientAR"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.ColumnDefinitions>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="20*"/>
<RowDefinition Height="80*"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0" Grid.Column="0">
<StackPanel Orientation="Vertical">
<DockPanel VerticalAlignment="Top" Height="20" Panel.ZIndex="1">
<Menu Name="fileMenu" Width="Auto" DockPanel.Dock="Top">
<MenuItem Header="File">
<MenuItem Header="Open Account File" Click="menuOpenFile_Click"/>
<MenuItem Header="Exit" Click="menuExit_Click"/>
</MenuItem>
<MenuItem Header="Options">
<!--<MenuItem Header="Update" Click="update_Click"/>-->
<MenuItem Header="About" Click="about_Click"/>
</MenuItem>
</Menu>
</DockPanel>
<WrapPanel Orientation="Horizontal" HorizontalAlignment="Center" Height="Auto">
<StackPanel Width="Auto" Orientation="Horizontal" HorizontalAlignment="Center">
<Border BorderBrush="MediumAquamarine" BorderThickness="2">
<Label Name="AccountNumber"/>
</Border>
<Border BorderBrush="MediumAquamarine" BorderThickness="2">
<Label Name="AcctDesc"/>
</Border>
<Border BorderBrush="MediumAquamarine" BorderThickness="2">
<Label Name="Organization"/>
</Border>
</StackPanel>
</WrapPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<Label Margin="20,10,0,0" Content="Activity Date Time" />
<Label Margin="60,10,0,0" Content="Beginning Balance" />
<Label Margin="10,10,0,0" Content="Charge Amount" />
<Label Margin="30,10,0,0" Content="Adjustments" />
<Label Margin="40,10,0,0" Content="Payments" />
<Label Margin="60,10,0,0" Content="End Balance" />
</StackPanel>
</StackPanel>
</Grid>
<Grid Grid.Row="1" Grid.Column="0">
<TreeView Name="DABView">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type self:dailyAccountBalance}">
<StackPanel Orientation="Horizontal">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<TextBlock Width="150" Text="{Binding DabActivityDate}" />
<TextBlock Width="100" Margin="20,0,0,0" Text="{Binding BegBalance}" />
<TextBlock Width="100" Margin="20,0,0,0" Text="{Binding ChrgAmount}" />
<TextBlock Width="100" Margin="20,0,0,0" Text="{Binding AdjAmount}" />
<TextBlock Width="100" Margin="20,0,0,0" Text="{Binding PmtAmount}" />
<TextBlock Width="100" Margin="20,0,0,0" Text="{Binding EndBalance}" />
</StackPanel>
</StackPanel>
</HierarchicalDataTemplate>
<DataTemplate x:Key="TestChild">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<TextBlock Text="YOU IZ GOOD" />
</StackPanel>
</DataTemplate>
</TreeView.Resources>
</TreeView>
</Grid>
</Grid>
</Window>
My Main Window's codebehind:
using Client_Invoice_Auditor.CoreClientAR;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Client_Invoice_Auditor
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public string accountFile;
public MainWindow()
{
InitializeComponent();
OpenAccountFile();
}
private void OpenAccountFile()
{
accountFile = "";
//errorFilters.Clear();
// Displays an OpenFileDialog so the user can select a file.
Microsoft.Win32.OpenFileDialog openFileDialog1 = new Microsoft.Win32.OpenFileDialog();
openFileDialog1.Filter = "Account Files|*.acct";
openFileDialog1.Title = "Select an account file";
if (openFileDialog1.ShowDialog() == true)
{
// Assign the cursor in the Stream to the Form's Cursor property.
accountFile = openFileDialog1.FileName;
//openFileDialog1.Dispose();
}
if (accountFile == "")
{
/*System.Windows.MessageBox.Show("File must be selected in order to continue - exiting now."
, "No File Selected", MessageBoxButton.OK);
this.Close();*/
if (!AcctDesc.HasContent)
{
AcctDesc.Content = "No account file Loaded";
//Version version = Assembly.GetExecutingAssembly().GetName().Version;
}
}
else
{
//openFileDialog1 = null;
Console.WriteLine("Account file path is: " + accountFile);
DataTable dataAR = new DataTable();
try
{
Tuple<accountARHeader, List<dailyAccountBalance>, DataTable> loadedAR = dabARLoader.LoadARData(accountFile);
//dataAR = loadedAR.Item2;
AccountNumber.Content = "Account Number: " + loadedAR.Item1.AccountNumber;
AcctDesc.Content = "Description: " + loadedAR.Item1.AccountDescription;
Organization.Content = "Client Organization: " + loadedAR.Item1.OrganizationName;
//TreeViewItem dummy = new TreeViewItem();
//dummy.DataContext = "Hi";
//loadedAR.Item2.First().Dummy.Add("La");
//DABView.Items.Add(dummy);
DABView.ItemsSource = loadedAR.Item2;
//DABView.DisplayMemberPath = "A";
}
catch (Exception e)
{
System.Windows.MessageBox.Show("I don't wanna open this file! Try another. Error: " + e.Message);
OpenAccountFile();
}
}
}
private void menuOpenFile_Click(object sender, RoutedEventArgs e)
{
OpenAccountFile();
}
private void menuExit_Click(object sender, RoutedEventArgs e)
{
Close();
}
private void about_Click(object sender, RoutedEventArgs e)
{
System.Windows.MessageBox.Show("I heard you like clicking buttons.");
}
}
}
And finally, my relevant class file:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls;
namespace Client_Invoice_Auditor.CoreClientAR
{
public class dailyAccountBalance
{
double dabID;
DateTime dabActivityDate;
double begBalance;
double endBalance;
double chrgAmount;
double adjAmount;
double pmtAmount;
string dabActivitySegment;
public double DabID { get => dabID; set => dabID = value; }
public double BegBalance { get => begBalance; set => begBalance = value; }
public double EndBalance { get => endBalance; set => endBalance = value; }
public string DabActivitySegment { get => dabActivitySegment; set => dabActivitySegment = value; }
public DateTime DabActivityDate { get => dabActivityDate; set => dabActivityDate = value; }
public double ChrgAmount { get => chrgAmount; set => chrgAmount = value; }
public double AdjAmount { get => adjAmount; set => adjAmount = value; }
public double PmtAmount { get => pmtAmount; set => pmtAmount = value; }
public ObservableCollection<dailyAccountBalance> Items { get; set; }
public List<string> Dummy { get => dummy; set => dummy = value; }
public IList<TreeViewItem> DummyItems { get => dummyItems; set => dummyItems = value; }
public dailyAccountBalance()
{
this.Items = new ObservableCollection<dailyAccountBalance>();
}
List<string> dummy = new List<string>();
IList<TreeViewItem> dummyItems;
}
}
Is there a way, for instance, that I can show a dummy string as ONE child in the TreeView when it expands? Do I need to implement expand methods for this?
Thanks so much in advance.
Typically what I've done in the past is add a DummyNode to the TreeNode, bind the "IsExpanded" TreeViewItem property in the TreeViewItem style to a IsExpanded property on my Node class in my View Model, and add a bool _isLoaded flag. Then when the IsExpanded it triggered I check if _isLoaded is true, and if not I then go and load all of the subsequent data and set the _isLoaded to true.
btw... I use Prism to give me my BindableBase class that implements the INotifyPropertyChanged event for OnPropertyChanged. You can choose to implement that yourself instead of using BindableBase.
Example:
Node Class
public class TreeData : BindableBase
{
private bool _isExpanded = false;
private bool _isSelected = false;
private bool _hasChildren = false;
private Func<List<TreeData>> _getChildrenMethod;
private bool _isLoaded = false;
public TreeData(string value, bool hasChildren, Func<List<TreeData>> getChildren)
{
Value = value;
_hasChildren = hasChildren;
_getChildrenMethod = getChildren;
if(hasChildren)
{
Children.Add(new TreeNode("Dummy", false, null));
}
}
public ObservableCollection<TreeData> Children { get; set; } = new ObservableCollection<TreeData>();
public string Value {get;set;}
public bool IsSelected
{
get { return _isSelected; }
set { _isSelected = value; OnPropertyChanged(); }
}
public bool IsExpanded
{
get { return _isExpanded; }
set
{
_isExpanded = value;
if(!_isLoaded)
{
// Call load method here
if(_addChildrenMethod != null && _hasChildren)
{
Children.Clear();
Children.AddMany(_addChildrenMethod());
}
_isLoaded = true;
}
OnPropertyChanged();
}
}
}
View Model Class (owns the list of items)
public class BusinessViewModel
{
public ObservableCollection<TreeData> Items { get; set; } = new ObservableCollection<TreeData>();
public BusinessViewModel()
{
Items.Add(new TreeData("Root Item", hasChildren: true, GetChildren));
}
public List<TreeData> GetChildren()
{
return new List<TreeData>()
{
new TreeData("Child", hasChildren: false, null);
};
}
}
Then the XAML
<TreeView ItemSource="{Binding Items}" >
<TreeView.Resources>
<Style TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="Blue" />
<Setter Property="Foreground" Value="White" />
</Trigger>
</Style.Triggers>
</Style>
</TreeView.Resources>
</TreeView>
Hope that helps :D
Related
So here I have a MVVM form. the Form contains a Datagrid which is connected to the Databank. I also have a ComboBox which I want to use as a filter option. The Filter option shoud filter by the "AnlV nr" so when the user selects "01" from the ComboBox the datagrid should refresh and show only that "AnlV nr" that have "01" Below I will share you the code and you can see that i've gotten as far as showing the "AnlV" values in the ComboBox but I now do not know how to do the rest and make the filter work. Below is my Viewmodel and the Xaml code.
If anyone can help me with this I would really apreciate it.
Xaml Code:
<Window x:Class="QBondsFrontend.Views.Input.AnlVTexteView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:QBondsFrontend.Views.Input" xmlns:input="clr-namespace:QBondsFrontend.ViewModels.Input" d:DataContext="{d:DesignInstance Type=input:AnlVTexteViewModel}"
mc:Ignorable="d"
Title="AnlVTexteView"
Width="800"
MinHeight="400"
Height="490"
MinWidth="1010"
MaxWidth="1010"
UseLayoutRounding="True">
<Grid Background="#A8A8A8" >
<StackPanel VerticalAlignment="Top" Background="#A8A8A8" Orientation="Horizontal" Height="57">
<Label
Content="AnlV Nr.:" Height="35" FontSize="12"/>
<ComboBox Background="LightGray" Height="20" Width="70" ItemsSource="{Binding lstAnlVTexte}" SelectedItem="{Binding Search}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding AnlVPara}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Button Height="18" Width="68" Margin="5, 0"
Content="Filter löschen" FontSize="11" Style="{StaticResource STL_ButtonStandard}"
x:Name="filterlöschen"
Command="{Binding icdFilterDelete}"/>
</StackPanel>
<StackPanel Background="LightGray" VerticalAlignment="Top" Height="177" Margin="0,57,0,0">
<DataGrid x:Name="datagridXAML"
Height="177"
ItemsSource="{Binding lstAnlVTexte, Mode=TwoWay}"
Style="{StaticResource STL_DataGridReporting}"
CellStyle="{StaticResource STL_DataGridCellReporting}"
ColumnHeaderStyle="{StaticResource STL_DataGridColumnHeaderReporting}"
AlternatingRowBackground="#A8A8A8"
CanUserResizeColumns="False"
>
<DataGrid.Columns>
<DataGridTextColumn Header="AnlV-Nr"
Binding="{Binding AnlVPara}"
Width="60"/>
<DataGridTextColumn Header="gültig ab"
Binding="{Binding TextGueltigAb}"
Width="68"/>
<DataGridTextColumn Header="Text"
Binding="{Binding ParaText}"
Width="750"/>
<DataGridTextColumn Header="Info"
Binding="{Binding Info}"
Width="*"/>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
<StackPanel Background="#A8A8A8" HorizontalAlignment="Center" Margin="10,268,0,141" Width="1010" >
<Label Content="Bearbeitungsbereich" FontWeight="Bold" FontSize="12" Height="33" />
</StackPanel>
<StackPanel>
<StackPanel Orientation="Horizontal" Background="#A8A8A8" HorizontalAlignment="Center"
Width="1010" Margin="0,294,0,0" Height="31">
<Label Height="25" Width="60" Margin="20, 0, 0, 0" Content="AnlV-Nr.:" />
<ComboBox IsEditable="True" Background="gray" Height="22" Width="69" ItemsSource="{Binding AnlVPara}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding lstAnlVTexte}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<CheckBox Height="15" Margin="10, 0, 0, 0" />
<Label Height="26" Width="122" Content="Editierwarnungen" />
<StackPanel Height="48" Width="100"/>
</StackPanel>
<StackPanel Height="22" Orientation="Horizontal">
<Label Margin="20, 0, 0, 0" Content="gültig ab:" Height="27" />
<TextBox Background="LightGray" Height="20" Width="100" />
</StackPanel>
<StackPanel Height="50" Orientation="Horizontal">
<Label Content="Text:" Height="27" Width="38" Margin="42,0,0,10" />
<TextBox Background="LightGray" Width="500" Height="43" />
</StackPanel>
<StackPanel Orientation="Horizontal" >
<Label Content="Info:" Height="27" Width="38" Margin="42,0,0,0" />
<TextBox Background="LightGray" Width="500" Height="20" />
<Button x:Name="BTN_speichern" Width="80" Height="18" Margin="20,0,0,0" Content="Speichern"
Style="{StaticResource STL_ButtonStandard}" Command="{Binding icdSpeichern}"/>
</StackPanel>
</StackPanel>
</Grid>
</Window>
ViewModel:
using Newtonsoft.Json;
using QBondsData.DBModels;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Linq;
namespace QBondsFrontend.ViewModels.Input
{
public class AnlVTexteViewModel : BaseViewModel
{
#region data
private string _AnlVPara;
private DateTime _TextGueltigAb;
private string _ParaText;
private string _Info;
private List<AnlVhistText> _lstAnlVTexte;
private string _search;
#endregion
#region constructor
public AnlVTexteViewModel()
{
icdFilterDelete = new RelayCommand<object>(parameter => filterdelete(), parameter => true);
icdSpeichern = new RelayCommand<object>(parameter => speichern(), parameter => true);
GetData();
}
#endregion
#region members
public ICommand icdFilterDelete { get; set; }
public ICommand icdSpeichern { get; set; }
private string Search
{
get { return _search; }
set
{
_search = value;
OnPropertyChanged("Search");
}
}
public string AnlVPara
{
get
{
return _AnlVPara;
}
set
{
_AnlVPara = value;
OnPropertyChanged("AnlVPara");
}
}
public DateTime TextGueltigAb
{
get
{
return _TextGueltigAb;
}
set
{
_TextGueltigAb = value;
OnPropertyChanged("TextGueltigAb");
}
}
public string ParaText
{
get
{
return _ParaText;
}
set
{
_ParaText = value;
OnPropertyChanged("ParaText");
}
}
public string Info
{
get
{
return _Info;
}
set
{
_Info = value;
OnPropertyChanged("Info");
}
}
public List<AnlVhistText> lstAnlVTexte
{
get { return _lstAnlVTexte; }
set
{
_lstAnlVTexte = value;
OnPropertyChanged("lstAnlVTexte");
}
}
#endregion
#region methods
private void filterdelete()
{
}
private void speichern()
{
}
private async Task GetData()
{
HttpResponseMessage Response = await Globals.SendRequest("AnlVTexte/GetAnlVTexte"
, "GET");
if (Response.IsSuccessStatusCode)
{
lstAnlVTexte = JsonConvert.DeserializeObject<List<AnlVhistText>>
(await Response.Content.ReadAsStringAsync());
}
else
{
lstAnlVTexte = new List<AnlVhistText>();
Application.Current.Dispatcher.Invoke((Action)delegate
{
Globals.CloseReportByHash(this.GetHashCode()
, "Fehler! (HTTP Status " + Response.StatusCode + ")." +
"Kontaktieren Sie den Support.");
});
}
}
#endregion
}
}
When you change the type of lstAnlVTexte to ICollectionView you get two events CurrentChanged and CurrentChanging which you can handle in your viewmodel. From the ICollectionView you can get the CurrentItem.
Like this:
private List<AnlVhistText> _anlVTexte = new List<AnlVhistText>();
public AnlVTexteViewModel()
{
[...]
lstAnlVTexte = new CollectionView(_anlVTexte);
lstAnlVTexte.CurrentChanged += SelectionChanged; // raised after the current item has been changed.
lstAnlVTexte.CurrentChanging += SelectionChanging; // raise before changing the current item. Event handler can cancel this event.
}
private void SelectionChanged(object sender, EventArgs e)
{
var selectedItem = lstAnlVTexte.CurrentItem;
}
private void SelectionChanging(object sender, CurrentChangingEventArgs e)
{
}
private ICollectionView _lstAnlVTexte;
public ICollectionView lstAnlVTexte
{
get { return _lstAnlVTexte; }
set
{
_lstAnlVTexte = value;
OnPropertyChanged("lstAnlVTexte");
}
}
Here's a sample using the community toolkit mvvm and linq.
If you're not familiar, the toolkit does code generation.
This is a simple scenario to illustrate the approach.
Mainwindowviewmodel.
public partial class MainWindowViewModel : ObservableObject
{
[ObservableProperty]
private int selectedFilterInt = -1;
partial void OnSelectedFilterIntChanged(int newValue)
{
FilteredList = new ObservableCollection<MyItem>(FullList.Where(x=>x.Category == selectedFilterInt).ToList());
}
public List<int> FilterOptions { get; set; } = new List<int> {1,2,3};
private List<MyItem> FullList= new List<MyItem>
{
new MyItem{ Category = 1},
new MyItem{ Category = 1},
new MyItem { Category = 1 },
new MyItem { Category = 2 },
new MyItem { Category = 2 },
new MyItem { Category = 3 }
};
[ObservableProperty]
private ObservableCollection<MyItem> filteredList = new ObservableCollection<MyItem>();
public MainWindowViewModel()
{
FilteredList = new ObservableCollection<MyItem>(FullList);
}
}
There's a full list of all the items.
But a filtered list is going to be bound to the itemssource of my listbox ( equivalent to your datagrid ).
Due to the code generated, when selectedFilterInt changes, OnSelectedFilterIntChanged will be called. It's got a handler listening for property changed of SelectedFilterInt if you dug into the generated code.
That method uses a linq where to filter the full list into filtered list.
Setting that filtered list property raises property changed and the view re reads the new collection.
MainWindow. ( I did mention this is simplified )
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Grid>
<StackPanel>
<ComboBox SelectedItem="{Binding SelectedFilterInt}"
ItemsSource="{Binding FilterOptions}"/>
<ListBox ItemsSource="{Binding FilteredList}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Category}"/>
<TextBlock Text="{Binding Comment}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</Grid>
</Window>
and MyItem
public partial class MyItem : ObservableObject
{
[ObservableProperty]
private int category = 0;
[ObservableProperty]
private string comment = "Some test string";
}
Which is a bit underwhelming visually but works:
In your code you need to get all the data into a collection.
Call that FulList.
You then need another collection which will be the filtered data.
Call this FilteredList.
Bind itemssource to FilteredList
Initially, you presumably want FilteredList to be = FullList
Then when the user selects something in the combobox you need to react to that.
You could bind selecteditem to a property and act in the setter or handle propertychanged like my code does.
However you do it, you get a new integer.
You then use that to filter FullList into a new collection which will replace the bound FilteredList.
You also need to somehow have one entry per AnlV nr whatever that is in your combobox.
AnlV nr isn't going to work as a property name since it's got a space but it is the equivalent to Category in my sample.
You will use that selected value in the linq.
Substitute the name of that property for Category. Substitute the type of whatever your collection is. Maybe that's AnlVhistText. I'm not sure.
I have a problem with DataGrid and after week trying to solve this I am out of mind.
I am using Prism.MVVM to handle loading properties, INotifyPropertyChanged etc.
My datagrid is being populated from database (EF) by of course ViewModel. When I double click on the row edit window will open with populated fields etc. I am doing this by "SelectedItem". Everything to this moment is working fine, but:
• When I editing my "Stock" textbox I see in the ProductListView window that this value is changing in realtime and even if I hit Cancel (and the window closed) it stays as I left it in ProductView and even after opening edit window again "Stock" value remain wrong, but in database the value is correct.
• When I edit for example "Category" or "Name" I do not see changes in datagrid in realtime (in this case values in DataGrid stay correct), but if I hit Cancel (and the window closed) an reopen edit window again value remain wrong, but in database the value is correct.
I tried to DeepCopy it and then Override SelectedItem back after edit and it work for database (it getting updated), but view (DataGrid) does not.
ProductListView:
<DataGrid ColumnWidth="Auto" Grid.Row="1" ItemsSource="{Binding ProductsCollectionView, UpdateSourceTrigger=PropertyChanged}"
SelectedItem="{Binding SelectedItem}" AutoGenerateColumns="False" IsReadOnly="True"
ScrollViewer.CanContentScroll="True" ScrollViewer.VerticalScrollBarVisibility="Visible" ScrollViewer.HorizontalScrollBarVisibility="Auto">
Methods/Commands responsible for edit in ProductListViewModel:
private Product _selectedItem;
public Product SelectedItem
{
get => _selectedItem;
set => SetProperty(ref _selectedItem, value);
}
public ProductListViewModel() // Contructor
{
_service = new ProductService();
Load();
ProductsCollectionView = CollectionViewSource.GetDefaultView(Data);
ProductsCollectionView.Filter = FilterProducts;
LoadCommands();
EditCommand = new DelegateCommand(Edit);
}
public ICommand EditCommand { get; set; }
private void Edit()
{
var dialog = new ProductView(SelectedItem);
dialog.ShowDialog();
if (dialog.DialogResult == true)
{
_service.UpdateProductData(SelectedItem);
_service.SaveChanges();
}
}
ProductView (Btw. I am using same View for Adding and Editing products, that is why I passing "SeleectedItem through the constructor)
<StackPanel Margin="0,0,13,0" Grid.Column="0">
<TextBlock Text="Product type" Style="{StaticResource StackPanelTextBox}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="41*" />
<ColumnDefinition Width="251*"/>
<ColumnDefinition Width="31*" />
</Grid.ColumnDefinitions>
<ComboBox IsEditable="True" IsTextSearchEnabled="True" ItemsSource="{Binding ProductTypes}" DisplayMemberPath="Name" SelectedValuePath="ProductTypeId" SelectedValue="{Binding Data.ProductTypeId, UpdateSourceTrigger=PropertyChanged}" Grid.ColumnSpan="2"/>
<Button Grid.Column="1" Command="{Binding AddProductTypeCommand}" FontSize="12" Padding="0" Height="15" Background="Transparent" Grid.ColumnSpan="2" Margin="251,6,0,6"/>
</Grid>
</StackPanel>
<StackPanel Margin="0,0,13,0" Grid.Column="1">
<TextBlock Text="Signature" Style="{StaticResource StackPanelTextBox}"/>
<TextBox Text="{Binding Data.Signature}"/>
</StackPanel>
<StackPanel Margin="0,0,0,0" Grid.Column="2">
<TextBlock Text="EAN" Style="{StaticResource StackPanelTextBox}"/>
<TextBox MaxLength="13" Text="{Binding Data.Ean}"/>
</StackPanel>
<Grid Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80*" />
<ColumnDefinition Width="20*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Grid.Column="0" Grid.Row="0" Margin="0,7,13,10">
<TextBlock Text="Name" Style="{StaticResource StackPanelTextBox}"/>
<TextBox x:Name="tbName" Text="{Binding Data.Name}"/>
</StackPanel>
<StackPanel Grid.Column="1" Grid.Row="0" Margin="0,0,0,0" VerticalAlignment="Center">
<TextBlock Text="Stock"/>
<mah:NumericUpDown Margin="0,3,0,3" Minimum="0" Interval="1" Value="{Binding Data.InStock}" />
</StackPanel>
</Grid>
ProductView.xaml.cs
public ProductView(Product product)
{
InitializeComponent();
var viewModel = new ProductViewModel(product);
WindowStartupLocation = WindowStartupLocation.CenterScreen;
viewModel.SaveAction = () =>
{
DialogResult = true;
};
viewModel.CancelAction = () =>
{
DialogResult = false;
};
DataContext = viewModel;
}
ProductViewModel
public Action SaveAction { get; set; }
public Action CancelAction { get; set; }
public ICommand Save { get; set; }
public ICommand Cancel { get; set; }
private Product _data;
public Product Data
{
get => _data;
set => SetProperty(ref _data, value);
}
public ProductViewModel(Product data)
{
LoadSellers();
LoadProductTypes();
LoadPackages();
Data = data;
NettoPrice = Data.PurchasePrice;
Save = new DelegateCommand(() => SaveAction?.Invoke());
Cancel = new DelegateCommand(() => CancelAction?.Invoke());
HideData = new DelegateCommand(HideMethod);
AddProductTypeCommand = new DelegateCommand(AddProductType);
EyeColor = #"..\Resources\Images\Eye-grey-48.png";
}
You need an extra data layer if you want to implement edit and cancel behavior of a data model. You can do this by implementing the IEditableObject interface.
You should never open the dialog from the view model. Instead, open it from a Button.Click handler in the code-behind. Define a DataTemplate for the dialog and assign it to the Window.ContentTemplate property. Also make sure that the database handling is implemented inside the model.
The following example shows how to display a reusable EditDialog (that operates on IEditableObject implementations) from the view. The example also shows how to cancel or commit data changes to the data model.
App.xaml
<DataTemplate DataType="{x:Type local:Product}">
<TextBox Text="{Binding Signature}" />
</DataTemplate>
EditDialog.xaml
Reusable dialog. Simply define a DataTemplate for the Window.ContentTemplate property to change the hosted Content.
Assign the data to be edited to the EditDialog.DataContext. This dialog can host any data that implements IEditableObject.
<Window Content="{Binding}">
<Window.Template>
<ControlTemplate TargetType="Window">
<Grid Background="{TemplateBinding Background}">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<AdornerDecorator Grid.Row="0">
<ContentPresenter />
</AdornerDecorator>
<!-- Dialog chrome -->
<Grid Grid.Row="1">
<StackPanel Orientation="Horizontal">
<Button Content="Commit"
Click="OnOkButtonClicked" />
<Button Content="Cancel"
Click="OnCancelButtonClicked"
IsCancel="True"
IsDefault="True" />
</StackPanel>
<ResizeGrip x:Name="WindowResizeGrip"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Visibility="Collapsed"
IsTabStop="false" />
</Grid>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="ResizeMode"
Value="CanResizeWithGrip">
<Setter TargetName="WindowResizeGrip"
Property="Visibility"
Value="Visible" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Window.Template>
</Window>
EditDialog.xaml.cs
public partial class EditDialog : Window
{
public EditDialog()
{
InitializeComponent();
this.Loaded += OnLoaded;
}
// In case the Window is closed using the chrome button
protected override void OnClosing(CancelEventArgs e)
{
if (this.Content is IEditableObject editableObject)
{
editableObject.CancelEdit();
}
base.OnClosing(e);
}
private void OnLoaded(object sender, EventArgs e)
{
// Content is set via data binding
if (this.Content is IEditableObject editableObject)
{
editableObject.BeginEdit();
}
}
private void OnCancelButtonClicked(object sender, RoutedEventArgs e)
{
if (this.Content is IEditableObject editableObject)
{
editableObject.CancelEdit();
}
this.Close();
}
private void OnOkButtonClicked(object sender, RoutedEventArgs e)
{
if (this.Content is IEditableObject editableObject)
{
editableObject.EndEdit();
}
this.Close();
}
}
MainWindow.xaml
<DataGrid ItemsSource="{Binding ProductsCollectionView}"
SelectedItem="{Binding SelectedItem}">
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<EventSetter Event="MouseDoubleClick"
Handler="DataGridRow_MouseDoubleClick" />
</Style>
</DataGrid.RowStyle>
</DataGrid>
MainWindow.xaml.cs
partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new ProductListViewModel();
}
private void DataGridRow_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
var productListViewModel = this.DataContext as ProductListViewModel;
// Alternatively, create a e.g., EditItem dependency property
// and bind it to the DataGrid.SelectedItem
Product editItem = productListViewModel.SelectedItem;
var editDialog = new EditDialog()
{
DataContext = editItem
};
editDialog.ShowDialog();
}
}
Product.cs
class Product : INotifyPropertyChanged, IEditableObject
{
internal class ProductData
{
// Use object.MemberwiseClone to create a shallow copy
public ProductData Clone() => MemberwiseClone() as ProductData;
public string Signature { get; set; }
}
public Product()
{
this.EditData = new ProductData();
this.BackupData = new ProductData();
}
public void BeginEdit()
{
if (this.IsInEditMode)
{
// Consider to throw an exception
return;
}
// Creates a shallow copy.
// If required, use a copy constructor to create a deep copy.
this.BackupData = this.EditData.Clone();
this.IsInEditMode = true;
}
public void CancelEdit()
{
if (!this.IsInEditMode)
{
// Consider to throw an exception
return;
}
this.EditData = this.BackupData;
this.IsInEditMode = false;
// Raise change notification for all public properties
// to undo potential binding changes
OnPropertyChanged(null);
}
public void EndEdit()
{
this.IsInEditMode = false;
}
public string Signature
{
get => this.EditData.Signature;
set
{
this.EditData.Signature = value;
OnPropertyChanged();
}
}
public bool IsInEditMode { get; private set; }
private ProductData BackupData { get; set; }
private ProductData EditData { get; set; }
}
I have the following view here where the user is able to input three different items: LockedOutBy, LockedOutFor, and LockedOutDate. I am having trouble creating a method within my view-model to allow the user to input these three items and once they hit 'ok', it will save the three items. Sorry if it is vague or if there is not enough info, please let me know if there is anything else needed.
Thank you.
View
<TextBlock VerticalAlignment="Center" Margin="5,5" Grid.Column="0" Grid.Row="1" Text="Locked Out By:"/>
<TextBox Grid.Column="1" Grid.Row="1" Text="{Binding LockedOutBy, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock VerticalAlignment="Center" Margin="5,5" Grid.Column="0" Grid.Row="2" Text="Locked Out For:"/>
<TextBox Grid.Column="1" Grid.Row="2" Text="{Binding LockedOutFor, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock VerticalAlignment="Center" Margin="5,5" Grid.Column="0" Grid.Row="3" Text="Locked Out Date:"/>
<TextBox Grid.Column="1" Grid.Row="3" Text="{Binding LockedOutDate, UpdateSourceTrigger=PropertyChanged}"/>
<Button Command="{Binding Path=OKCommand}"
View-Model
This is what I have so far in my view-model.
private string _LockedOutFor;
public string LockedOutFor
{
get { return _LockedOutFor; }
set
{
_LockedOutFor = value;
OnPropertyChanged("OwnerName");
}
}
private string _LockedOutBy;
public string LockedOutBy
{
get { return _LockedOutBy; }
set
{
_LockedOutBy = value;
OnPropertyChanged("Street");
}
}
private int _LockedOutDate;
public int LockedOutDate
{
get { return _LockedOutDate; }
set
{
_LockedOutDate = value;
OnPropertyChanged("StreetOverflow");
}
}
public ICommand CancelCommand
{
get { return new RelayCommand(c => OnCancelLock()); }
}
public ICommand OKCommand
{
get { return new RelayCommand(c => OnOKLock()); }
}
protected void OnOKLock()
{
OnOK(LockedOutFor, LockedOutBy, LockedOutDate);
}
protected void OnCancelLock()
{
OnCancel();
}
ANotifyPropertyChanged - base implementation for all of our view models
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace SO60269403
{
public abstract class ANotifyPropertyChanged : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void SetProperty<T>(ref T backingStore, T newValue, [CallerMemberName] string propertyName = "")
{
var areEqual = ReferenceEquals(backingStore, newValue);
if (areEqual)
return;
backingStore = newValue;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
LockedOutViewModel - our view model
namespace SO60269403
{
public class LockedOutViewModel : ANotifyPropertyChanged
{
private string lockedOutBy;
private string lockedOutFor;
private int lockedOutDate;
public string LockedOutBy { get => lockedOutBy; set => SetProperty(ref lockedOutBy, value); }
public string LockedOutFor { get => lockedOutFor; set => SetProperty(ref lockedOutFor, value); }
public int LockedOutDate { get => lockedOutDate; set => SetProperty(ref lockedOutDate, value); }
}
}
ACommand - base implementation for all of our commands
using System;
using System.Windows.Input;
namespace SO60269403
{
public abstract class ACommand : ICommand
{
public event EventHandler CanExecuteChanged;
public virtual bool CanExecute(object parameter) => true;
public abstract void Execute(object parameter);
}
}
SaveCommand - our save command
namespace SO60269403
{
public class SaveCommand : ACommand
{
public LockedOutViewModel LockedOutViewModel { get; set; }
public override void Execute(object parameter)
{
//TODO
}
}
}
MainWindow - a view to data-bind our view model and hook up our save command
<Window
x:Class="SO60269403.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SO60269403">
<Window.Resources>
<local:LockedOutViewModel
x:Key="LockedOutViewModel" />
</Window.Resources>
<Grid
DataContext="{StaticResource LockedOutViewModel}">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Row="0"
Grid.Column="0">Locked Out By</TextBlock>
<TextBlock
Grid.Row="1"
Grid.Column="0">Locked Out For</TextBlock>
<TextBlock
Grid.Row="2"
Grid.Column="0">Locked Out Date</TextBlock>
<TextBox
Grid.Row="0"
Grid.Column="1"
Text="{Binding LockedOutBy}" />
<TextBox
Grid.Row="1"
Grid.Column="1"
Text="{Binding LockedOutFor}" />
<TextBox
Grid.Row="2"
Grid.Column="1"
Text="{Binding LockedOutDate}" />
<StackPanel
Grid.Row="3"
Grid.Column="0"
Grid.ColumnSpan="2"
Orientation="Horizontal"
HorizontalAlignment="Right">
<StackPanel.Resources>
<Style
TargetType="Button">
<Setter
Property="Width"
Value="100" />
</Style>
</StackPanel.Resources>
<Button>Cancel</Button>
<Button>
<Button.Command>
<local:SaveCommand
LockedOutViewModel="{StaticResource LockedOutViewModel}" />
</Button.Command>
OK
</Button>
</StackPanel>
</Grid>
</Window>
if we put a breakpoint in SaveCommand.Execute then run the app, fill in some values and click the OK button, the debugger should break on our breakpoint and we can observe that the view model's properties match what we entered. this demonstrates that the data is in place ready to be saved.
I have created a WPF app that has an ObservableCollection>. This is bound to a ListBox which has a DataTemplate to display the information in a tidy manner.
When I run the app the ListBox populates the rows as expected... but no information is displayed in the DataTemplate.
Here are the Code Parts
WINDOW XAML CODE
<Window
x:Class="web.app.smash.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:web.app.smash"
xmlns:m="clr-namespace:web.app.smash.lib.Helpers;assembly=web.app.smash.lib"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:Name="MainPage"
Title="MainWindow"
Width="800"
Height="450"
mc:Ignorable="d">
<Window.Resources>
<DataTemplate x:Key="ResultListItemTemplate" DataType="{x:Type m:ProcessedURLResult}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="12" />
<RowDefinition Height="12" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<TextBlock
FontSize="8"
FontWeight="Bold"
Text="Start time:" />
<TextBlock
x:Name="StartTime"
Width="50"
Margin="4,0,0,0"
FontSize="8"
Text="{Binding StartTime, Mode=OneWay}" />
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal">
<TextBlock
FontSize="8"
FontWeight="Bold"
Text="End time:" />
<TextBlock
x:Name="EndTime"
Margin="4,0,0,0"
FontSize="8"
Text="{Binding EndTime, Mode=OneWay}" />
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal">
<TextBlock
FontSize="8"
FontWeight="Bold"
Text="Milli seconds:" />
<TextBlock
x:Name="MilliSecondsTaken"
Margin="4,0,0,0"
FontSize="8"
Text="{Binding MillisecondsTaken, Mode=OneWay}" />
</StackPanel>
<StackPanel
Grid.Row="1"
Grid.Column="1"
Orientation="Horizontal">
<TextBlock
FontSize="8"
FontWeight="Bold"
Text="HTTP ststus code:" />
<TextBlock
x:Name="HTTPStatusCode"
Margin="4,0,0,0"
FontSize="8"
Text="{Binding HTTPStatusCode, Mode=OneWay}" />
</StackPanel>
<StackPanel
Grid.Row="2"
Grid.ColumnSpan="2"
Orientation="Horizontal">
<TextBlock
FontSize="8"
FontWeight="Bold"
Text="Error message:" />
<TextBlock
x:Name="ErrorMessage"
Height="22"
Margin="4,0,0,0"
FontSize="8"
Text="{Binding ErrorMessage, Mode=OneWay}"
TextWrapping="Wrap" />
</StackPanel>
<StackPanel
Grid.Row="3"
Grid.ColumnSpan="2"
Orientation="Horizontal">
<TextBlock
FontSize="8"
FontWeight="Bold"
Text="API results" />
<TextBlock
x:Name="APIResults"
Height="42"
Margin="4,0,0,0"
FontSize="8"
Text="{Binding APIResults, Mode=OneWay}"
TextWrapping="Wrap" />
</StackPanel>
</Grid>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="32" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel
Grid.Row="0"
Grid.Column="0"
Orientation="Vertical">
<Button
x:Name="SubmitCustomerGet"
Margin="0,0,0,12"
Click="SubmitCustomerGet_Click"
Content="Get a Customer" />
<Button
x:Name="StartPerformanceTests"
Margin="0,0,0,4"
Click="StartPerformanceTests_Click"
Content="Start Tests" />
<Button
x:Name="StopPerformanceTests"
Margin="0,0,0,4"
Click="StopPerformanceTests_Click"
Content="Stop Tests" />
</StackPanel>
<StackPanel
Grid.Row="0"
Grid.Column="1"
Orientation="Vertical">
<TextBlock x:Name="CountURLAdded" Background="#FFFBFFA7" />
<TextBlock x:Name="CountURLWaiting" Background="#FFEA9393" />
<TextBlock x:Name="CountURLFinished" Background="#FFB7EEB1" />
</StackPanel>
<TextBlock
x:Name="InformationMessage"
Grid.Row="1"
Grid.ColumnSpan="2"
Background="#FF646464" />
<ListBox
x:Name="ResultList"
Grid.Row="2"
Grid.ColumnSpan="3"
ItemTemplate="{DynamicResource ResultListItemTemplate}"
ItemsSource="{Binding ElementName=MainPage, Path=AwesomeSauce, Mode=OneWay}" />
</Grid>
</Window>
WINDOW CODE BEHIND
using System;
using System.Timers;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using web.app.smash.lib;
using web.app.smash.lib.Helpers;
using System.Net.Http;
using System.Threading;
using Timer = System.Timers.Timer;
using System.Diagnostics;
using System.Collections.ObjectModel;
namespace web.app.smash
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
//This is the URL to the webAPI
private const string BASEURL = "http://localhost:23653/";
public ObservableCollection<Task<ProcessedURLResult>> AwesomeSauce
{
get
{ return ocDownloadedTasks; }
set
{ ocDownloadedTasks = value; }
}
private ObservableCollection<Task<ProcessedURLResult>> ocDownloadedTasks;
private WebRequestCustomer wc = new WebRequestCustomer();
private CancellationTokenSource cts;
private CancellationToken ct;
private HttpClient client = new HttpClient();
private long counturladded = 0;
private long counturlwaiting = 0;
private long counturlfinihed = 0;
private Timer smashtimer;
public MainWindow()
{
Debug.WriteLine("App started");
InitializeComponent();
SetupTimer(1000);
ocDownloadedTasks = new ObservableCollection<Task<ProcessedURLResult>>();
ocDownloadedTasks.CollectionChanged += OcDownloadedTasks_CollectionChanged;
ResultList.ItemsSource = ocDownloadedTasks;
}
private void OcDownloadedTasks_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
foreach(Task t in e.NewItems)
{
if(t.Status == TaskStatus.Created)
{
t.RunSynchronously();
}
}
}
private void SetupTimer(double interval)
{
Debug.WriteLine("Timer set to:" + interval.ToString());
smashtimer = new System.Timers.Timer(interval);
Debug.WriteLine("Timer event handler set");
smashtimer.Elapsed += Smashtimer_Elapsed;
smashtimer.AutoReset = true;
}
private void Smashtimer_Elapsed(object sender, ElapsedEventArgs e)
{
//Debug.WriteLine("Timer event handler elapsed start: " + e.SignalTime.ToString());
//this.urlList.Add(BASEURL);
//Debug.WriteLine("ProcessAll: Returns a collection of tasks (In Loop)");
//// ***Create a query that, when executed, returns a collection of tasks.
//TasksList = from url in this.urlList select wc.ProcessPostURL(url, client, ct);
//Debug.WriteLine("ProcessAll: Start processing the list of Tasks (In Loop)");
//downloadTasks.AddRange(TasksList.ToList());
//downloadTasks.Add(wc.ProcessPostURL(BASEURL, client, ct));
App.Current.Dispatcher.Invoke((Action)delegate
{
ocDownloadedTasks.Add(wc.ProcessPostURL(BASEURL, client, ct));
});
//TasksList = null;
counturladded += 1;
}
private async void SubmitCustomerGet_Click(object sender, RoutedEventArgs e)
{
await wc.GetCustomerByID(BASEURL, 6);
//ResponsesList.Inlines.Add(wc.DisplayResults);
//ResponsesList.Inlines.Add(new LineBreak());
InformationMessage.Text = "Get single customer";
}
private void StartPerformanceTests_Click(object sender, RoutedEventArgs e)
{
Debug.WriteLine("Start Performance button: Clicked");
Debug.WriteLine("Start Performance button: Create the cancelation token");
//Create the cancellation token
cts = new CancellationTokenSource();
ct = cts.Token;
Debug.WriteLine("Start Performance button: Timer started");
smashtimer.Start();
InformationMessage.Text = "Timer Started";
}
private void StopPerformanceTests_Click(object sender, RoutedEventArgs e)
{
Debug.WriteLine("Stop performance button: Clicked");
Debug.WriteLine("Stop performance button: Timer stopped");
smashtimer.Stop();
InformationMessage.Text = "Timer Stopped";
}
//private void DisplayResults(ProcessedURLResult pur)
//{
// StringBuilder sb = new StringBuilder();
// if (pur.ErrorMessage==null)
// {
// sb.Append("Milliseconds: " + pur.MillisecondsTaken.ToString());
// sb.Append("API Result: " + pur.APIResults);
// ResponsesList.Inlines.Add(sb.ToString());
// ResponsesList.Inlines.Add(new LineBreak());
// }
// else
// {
// sb.Append("Error: " + pur.ErrorMessage);
// ResponsesList.Inlines.Add(sb.ToString());
// ResponsesList.Inlines.Add(new LineBreak());
// }
// ResponsesList.InvalidateVisual();
//}
//private void DisplayInformation()
//{
// CountURLAdded.Text = counturladded.ToString();
// CountURLWaiting.Text = counturlwaiting.ToString();
// CountURLFinished.Text = counturlfinihed.ToString();
//}
}
}
PROCESSURLRESULTS CODE
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace web.app.smash.lib.Helpers
{
public class ProcessedURLResult : INotifyPropertyChanged
{
private string _apiresults;
private long _millisecondsTaken;
private DateTime _startTime;
private DateTime _endTime;
private string _hTTPStatusCode;
private string _errorMessage;
public string APIResults
{
get
{
return _apiresults;
}
set
{
_apiresults = value;
OnPropertyChanged(nameof(APIResults));
}
}
public long MillisecondsTaken
{
get
{
return _millisecondsTaken;
}
set
{
_millisecondsTaken = value;
OnPropertyChanged(nameof(MillisecondsTaken));
}
}
public DateTime StartTime
{
get
{
return _startTime;
}
set
{
_startTime = value;
OnPropertyChanged(nameof(StartTime));
}
}
public DateTime EndTime
{
get
{
return _endTime;
}
set
{
_endTime = value;
OnPropertyChanged(nameof(EndTime));
}
}
public string HTTPStatusCode
{
get
{
return _hTTPStatusCode;
}
set
{
_hTTPStatusCode = value;
OnPropertyChanged(nameof(HTTPStatusCode));
}
}
public string ErrorMessage
{
get
{
return _errorMessage;
}
set
{
_errorMessage = value;
OnPropertyChanged(nameof(ErrorMessage));
}
}
public event PropertyChangedEventHandler PropertyChanged;
// Create the OnPropertyChanged method to raise the event
private protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
}
Talking Points
The ObservableCollection holds Tasks of type ProcessURLResults, and to access these properties of ProcessURLResults you need to use the Results property of the task.
ocDownloadedTasks[0].Result.APIResults;
So how do I make the ListBox's DataTemplate get the Result property?
Tasks should be awaited.
Do not use Task<ProcessedURLResult> as item type. Instead, declare a (read-only) collection property like
public ObservableCollection<ProcessedURLResult> AwesomeSauce { get; } =
new ObservableCollection<ProcessedURLResult>();
and populate it in an async method by awaiting the Task returned from the ProcessPostURL method:
private async void Smashtimer_Elapsed(object sender, ElapsedEventArgs e)
{
...
var result = await wc.ProcessPostURL(BASEURL, client, ct)
Dispatcher.Invoke(() => AwesomeSauce.Add(result));
}
There is also no need to assign the ListBox's ItemsSource in code behind if you bind it in XAML:
<ListBox ItemsSource="{Binding ElementName=MainPage, Path=AwesomeSauce}" .../>
You may also want to replace System.Timers.Timer by DispatcherTimer to avoid the need for calling Dispatcher.Invoke.
I think that your template binding type mismatch.
<DataTemplate x:Key="ResultListItemTemplate" DataType="{x:Type m:ProcessedURLResult}">
is different from
public ObservableCollection<Task<ProcessedURLResult>> AwesomeSauce
and
ItemsSource="{Binding ElementName=MainPage, Path=AwesomeSauce, Mode=OneWay}" />
is redundant. you have the assign code in MainWindow()
ResultList.ItemsSource = ocDownloadedTasks;
try to use wrapper class. for instance
public class ResultWrapper
{
public Task<ProcessedURLResult> InnerTask { get; set; }
public ProcessedURLResult Result
{
get
{
return InnerTask.Result;
}
}
}
and
public ObservableCollection<ResultWrapper> AwesomeSauce
XAML is
<DataTemplate x:Key="ResultListItemTemplate" DataType="{x:Type ResultWrapper}">
...
<TextBlock
x:Name="StartTime"
Width="50"
Margin="4,0,0,0"
FontSize="8"
Text="{Binding Result.StartTime, Mode=OneWay}" />
....
You have to change the DataType to Task (or remove it completely) and then adjust the binding paths:
<DataTemplate DataType="{x:Type Task}">
<TextBlock Text="{Binding Result.ErrorMessage}"/>
</DataTemplate>
Note
You definitely should avoid Task.RunSynchronously() as there are scenarios where it will produce a deadlock. Task are designed for asynchronous or concurrent programming.
To have code execute synchronously and deferred at some random time you should make use of delegates and use one of the Action overloads (Action<ProcessedURLResult> in your scenario).
Then don't bind to this delegate collection directly (or Task collection) but to a dedicated result collection of type ProcessedURLResult you can bind the ItemsSource to. This will give you Intellisense support in XAML too, when writing the DataTemplate.
Whether you use Action or stick to Task you will experience UI freeze. Depending on the individual execution time and count of items, i.e. total execution time, this freeze will be more or less noticable but is always not desireable and should/ can be avoided. Therefore if you have access to WebRequestCustomer consider to make it run asynchronously (e.g., using TaskCompletionSource in case of posting HTTP requests).
Also your code is mixing both ways to populate the ItemsControl.ItemsSource: you are using Binding and direct assignment. The latter will override the former and behaves differently. I recommend Binding set from XAML.
How can I get my app to navigate to a specific page based on the item within the semantic zoom that was tapped? Each item has a link to its own page and I want to use the 'item.Link' element so that the app reads the link and uses it to navigate to the specified page.
MetropolitanDataSource.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Exits_Expert_London_Lite
{
using System;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Media;
// To significantly reduce the sample data footprint in your production application, you can set
// the DISABLE_SAMPLE_DATA conditional compilation constant and disable sample data at runtime.
#if DISABLE_SAMPLE_DATA
internal class SampleDataSource { }
#else
public class Item : System.ComponentModel.INotifyPropertyChanged
{
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
}
}
private string _Station = string.Empty;
public string Station
{
get
{
return this._Station;
}
set
{
if (this._Station != value)
{
this._Station = value;
this.OnPropertyChanged("Station");
}
}
}
private string _Zone = string.Empty;
public string Zone
{
get
{
return this._Zone;
}
set
{
if (this._Zone != value)
{
this._Zone = value;
this.OnPropertyChanged("Zone");
}
}
}
private string _Link = string.Empty;
public string Link
{
get
{
return this._Link;
}
set
{
if (this._Link != value)
{
this._Link = value;
this.OnPropertyChanged("Link");
}
}
}
}
public class GroupInfoList<T> : List<object>
{
public object Key { get; set; }
public new IEnumerator<object> GetEnumerator()
{
return (System.Collections.Generic.IEnumerator<object>)base.GetEnumerator();
}
}
public class StoreData
{
public StoreData()
{
Item item;
item = new Item();
item.Station = "Amersham";
item.Zone = "Fare zone 9";
item.Link = "/Lines and Stations/Metropolitan/AMR_(Metropolitan).xaml";
Collection.Add(item);
item = new Item();
item.Station = "Chalfont & Latimer";
item.Zone = "Fare zone 8";
item.Link = "/Lines and Stations/Metropolitan/CFO_(Metropolitan).xaml";
Collection.Add(item);
item = new Item();
item.Station = "Chesham";
item.Zone = "Fare zone 9";
item.Link = "/Lines and Stations/Metropolitan/Chesham.xaml";
Collection.Add(item);
}
private ItemCollection _Collection = new ItemCollection();
public ItemCollection Collection
{
get
{
return this._Collection;
}
}
internal List<GroupInfoList<object>> GetGroupsByCategory()
{
List<GroupInfoList<object>> groups = new List<GroupInfoList<object>>();
var query = from item in Collection
orderby ((Item)item).Zone
group item by ((Item)item).Zone into g
select new { GroupName = g.Key, Items = g };
foreach (var g in query)
{
GroupInfoList<object> info = new GroupInfoList<object>();
info.Key = g.GroupName;
foreach (var item in g.Items)
{
info.Add(item);
}
groups.Add(info);
}
return groups;
}
internal List<GroupInfoList<object>> GetGroupsByLetter()
{
List<GroupInfoList<object>> groups = new List<GroupInfoList<object>>();
var query = from item in Collection
orderby ((Item)item).Station
group item by ((Item)item).Station[0] into g
select new { GroupName = g.Key, Items = g };
foreach (var g in query)
{
GroupInfoList<object> info = new GroupInfoList<object>();
info.Key = g.GroupName;
foreach (var item in g.Items)
{
info.Add(item);
}
groups.Add(info);
}
return groups;
}
}
// Workaround: data binding works best with an enumeration of objects that does not implement IList
public class ItemCollection : IEnumerable<Object>
{
private System.Collections.ObjectModel.ObservableCollection<Item> itemCollection = new System.Collections.ObjectModel.ObservableCollection<Item>();
public IEnumerator<Object> GetEnumerator()
{
return itemCollection.GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public void Add(Item item)
{
itemCollection.Add(item);
}
}
#endif
}
Metropolitan_line.xaml.cs
using Exits_Expert_London_Lite.Common;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
// The Basic Page item template is documented at http://go.microsoft.com/fwlink/?LinkId=234237
namespace Exits_Expert_London_Lite.Lines_and_Stations.Metropolitan
{
public sealed partial class Metropolitan_line : Page
{
public Metropolitan_line()
{
this.InitializeComponent();
StoreData _storeData = null;
// creates a new instance of the sample data
_storeData = new StoreData();
// sets the list of categories to the groups from the sample data
List<GroupInfoList<object>> dataLetter = _storeData.GetGroupsByLetter();
// sets the CollectionViewSource in the XAML page resources to the data groups
cvsMetropolitan.Source = dataLetter;
// sets the items source for the zoomed out view to the group data as well
(semanticZoom.ZoomedOutView as ListViewBase).ItemsSource = cvsMetropolitan.View.CollectionGroups;
(semanticZoom.ZoomedInView as ListViewBase).SelectedIndex = -1;
}
#region Data Visualization
void ItemsGridView_ContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args)
{
ItemViewer iv = args.ItemContainer.ContentTemplateRoot as ItemViewer;
if (args.InRecycleQueue == true)
{
iv.ClearData();
}
else if (args.Phase == 0)
{
iv.ShowPlaceholder(args.Item as Item);
args.RegisterUpdateCallback(ContainerContentChangingDelegate);
}
else if (args.Phase == 1)
{
iv.ShowStation();
args.RegisterUpdateCallback(ContainerContentChangingDelegate);
}
else if (args.Phase == 2)
{
iv.ShowZone();
}
args.Handled = true;
}
private TypedEventHandler<ListViewBase, ContainerContentChangingEventArgs> ContainerContentChangingDelegate
{
get
{
if (_delegate == null)
{
_delegate = new TypedEventHandler<ListViewBase, ContainerContentChangingEventArgs>(ItemsGridView_ContainerContentChanging);
}
return _delegate;
}
}
private TypedEventHandler<ListViewBase, ContainerContentChangingEventArgs> _delegate;
#endregion //Data Visualization
private void backButton_Tapped(object sender, TappedRoutedEventArgs e)
{
this.Frame.Navigate(typeof(Station_Chooser));
}
void GridView_ItemClicked(object sender, ItemClickEventArgs args)
{
var item = (Item)args.ClickedItem;
if (item.Link == "/Lines and Stations/Metropolitan/AMR_(Metropolitan).xaml")
((Frame)Window.Current.Content).Navigate(typeof(AMR__Metropolitan_));
else if (item.Link == "/Lines and Stations/Metropolitan/CFO_(Metropolitan).xaml")
((Frame)Window.Current.Content).Navigate(typeof(CFO__Metropolitan_));
else if (item.Link == "/Lines and Stations/Metropolitan/Chesham.xaml")
((Frame)Window.Current.Content).Navigate(typeof(Chesham));
}
}
}
ItemViewer.xaml
<UserControl
x:Class="Exits_Expert_London_Lite.ItemViewer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Exits_Expert_London_Lite"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Margin="0,0,0,20" HorizontalAlignment="Left">
<StackPanel>
<StackPanel Margin="10,0,0,0" Width="420">
<TextBlock x:Name="stationTextBlock" Foreground="{StaticResource ApplicationForegroundThemeBrush}" VerticalAlignment="Center" HorizontalAlignment="Left" FontFamily="Segoe UI" FontSize="32" FontWeight="Light" />
<TextBlock x:Name="zoneTextBlock" TextWrapping="Wrap" Foreground="White" FontSize="20" FontWeight="Light" VerticalAlignment="Center" HorizontalAlignment="Left"/>
</StackPanel>
</StackPanel>
</Grid>
</UserControl>
Metropolitan_line.xaml
<Page
x:Class="Exits_Expert_London_Lite.Lines_and_Stations.Metropolitan.Metropolitan_line"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Exits_Expert_London_Lite"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Page.Resources>
<CollectionViewSource x:Name="cvsMetropolitan" IsSourceGrouped="true" />
</Page.Resources>
<Grid Background="Black">
<Grid.ChildrenTransitions>
<TransitionCollection>
<EntranceThemeTransition/>
</TransitionCollection>
</Grid.ChildrenTransitions>
<Grid.RowDefinitions>
<RowDefinition Height="140"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button x:Name="backButton" Margin="39,59,39,0"
Style="{StaticResource NavigationBackButtonNormalStyle}"
VerticalAlignment="Top"
AutomationProperties.Name="Back"
AutomationProperties.AutomationId="BackButton"
AutomationProperties.ItemType="Navigation Button" Tapped="backButton_Tapped"/>
<TextBlock x:Name="pageTitle" Text="Metropolitan line" Style="{StaticResource HeaderTextBlockStyle}" Grid.Column="1"
IsHitTestVisible="false" TextWrapping="NoWrap" VerticalAlignment="Bottom" Margin="0,0,30,40" Foreground="#FF9B0056"/>
</Grid>
<Grid x:Name="Output" Grid.Row="1">
<!-- This shows a hard-coded width to show within the SDK Sample framework. In a real application you likely wouldn't set a width on the SemanticZoom -->
<SemanticZoom x:Name="semanticZoom" Margin="0,0,0,0">
<SemanticZoom.ZoomedOutView>
<GridView ScrollViewer.IsHorizontalScrollChainingEnabled="False" >
<GridView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Group.Key}" HorizontalAlignment="Center" FontFamily="Segoe UI" FontWeight="Light" FontSize="56" Foreground="White"/>
</DataTemplate>
</GridView.ItemTemplate>
<GridView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsWrapGrid ItemWidth="200" ItemHeight="200" MaximumRowsOrColumns="4" VerticalAlignment="Center"/>
</ItemsPanelTemplate>
</GridView.ItemsPanel>
<GridView.ItemContainerStyle>
<Style TargetType="GridViewItem">
<Setter Property="Margin" Value="4" />
<Setter Property="Padding" Value="10" />
<Setter Property="Background" Value="#9B0056" />
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
</GridView.ItemContainerStyle>
</GridView>
</SemanticZoom.ZoomedOutView>
<SemanticZoom.ZoomedInView>
<GridView x:Name="ItemsGridView"
ItemsSource="{Binding Source={StaticResource cvsMetropolitan}}"
ShowsScrollingPlaceholders="False"
ContainerContentChanging="ItemsGridView_ContainerContentChanging"
IsSwipeEnabled="True" ScrollViewer.IsHorizontalScrollChainingEnabled="False" ItemClick="GridView_ItemClicked">
<GridView.ItemTemplate>
<DataTemplate>
<local:ItemViewer/>
</DataTemplate>
</GridView.ItemTemplate>
<GridView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsWrapGrid/>
</ItemsPanelTemplate>
</GridView.ItemsPanel>
<GridView.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock Text='{Binding Key}' Foreground="#FF9B0056" Margin="5" FontSize="50" FontFamily="Segoe UI" FontWeight="Light" />
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</GridView.GroupStyle>
</GridView>
</SemanticZoom.ZoomedInView>
</SemanticZoom>
</Grid>
</Grid>
</Page>
Here you go. In your code-behind...
void GridView_ItemClicked(object sender, ItemClickEventArgs args)
{
var item = (Item)args.ClickedItem;
if (item.Link == "/Page_1.xaml")
((Frame)Window.Current.Content).Navigate(typeof (Page_1));
else if (item.Link == "/Page_2.xaml")
((Frame)Window.Current.Content).Navigate(typeof (Page_2));
else if (item.Link == "/Page_3.xaml")
((Frame)Window.Current.Content).Navigate(typeof (Page_3));
}
Best of luck!