I have a text log File which i parse every 10 seconds to display it values on the WPF-Application, I am trying to use MVVM for the first time in WPF. The problem i am facing is I am unable to refresh the DataContext with the timer.
The format of the text file is
log.txt
UserID|RP1|MS9|1.25
UserID|RP5|MS7|1.03
Code for the Application is given below
Code for Model-Class
public class UserModel
{
public string userID{get; set;}
public string RP{get; set;}
public string MS{get; set;}
public string Rate{get; set;}
}
Code for ModelView-Class
public class AppModelView
{
private ObservableCollection<UserModel> _userList;
DispatcherTimer LogTimer;
public AppModelView()
{
_userList = new ObservableCollection<UserModel>();
LogTimer = new DispatcherTimer();
LogTimer.Interval = TimeSpan.FromMilliseconds(10000);
LogTimer.Tick += (s, e) =>
{
foreach(DataRow row in LogManager.Record) //LogManager is class which parse the txt file and assign value into a DataTable Record
_userList.add(new UserModel
{
userID= row[0].toString();
RP = row[1].toString();
MS = row[2].toString();
rate = row[3].toString();
});
};
LogTimer.Start();
}
public ObservableCollection<UserModel> UserList
{
get { return _userList; }
set { _userList = value;
NotifyPropertyChanged("UserList");}
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
MainWindows.xaml
<Window x:Class="MonitoringSystem.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Monitoring Server" WindowStartupLocation="CenterScreen" Height="768" WindowState="Maximized" >
<Grid>
<DockPanel>
<Label Content="User Monitored" DockPanel.Dock="Top"/>
<ListView Name="lstRpt" DockPanel.Dock="Bottom" ItemsSource="{Binding UserList}" >
<ListView.View>
<GridView>
<GridViewColumn Header="UserID" DisplayMemberBinding="{Binding userID}"/>
<GridViewColumn Header="RP" DisplayMemberBinding="{Binding RP}"/>
<GridViewColumn Header="MS" DisplayMemberBinding="{Binding MS}"/>
<GridViewColumn Header="Rate" DisplayMemberBinding="{Binding Rate}"/>
</GridView>
</ListView.View>
</ListView>
</DockPanel>
</Grid>
</Windows>
MainWindows.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
AppViewModel VM = new AppViewModel();
this.DataContext = VM;
}
}
Now if I remove the DispatcherTimer it display the values which were parse for the first time and display it , but with timer it cannot display any values.
Your Guidance is highly appreciated.
What I suspect is happening is that your UserModel is getting added to the collection before its properties have been set, and because your UserModel has no INPC the view never updates once they are set.
Try changing your code to:
LogTimer.Tick += (s, e) =>
{
foreach(DataRow row in LogManager.Record) //LogManager is class which parse the txt file and assign value into a DataTable Record
{
var userModel = new UserModel
{
userID= row[0].toString();
RP = row[1].toString();
MS = row[2].toString();
rate = row[3].toString();
};
_userList.Add(userModel);
};
LogTimer.Start();
};
Correction needed only in ViewModel
public class AppModelView
{
private ObservableCollection<UserModel> _userList;
DispatcherTimer LogTimer;
public AppModelView()
{
_userList = new ObservableCollection<UserModel>();
LogTimer = new DispatcherTimer();
LogTimer.Interval = TimeSpan.FromMilliseconds(10000);
LogTimer.Tick += (s, e) =>
{
Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Normal,
new Action(
delegate()
{
UserList.Add(new UserModel
{
userID = "test"
});
}
)
);
};
LogTimer.Start();
}
public ObservableCollection<UserModel> UserList
{
get { return _userList; }
set
{
_userList = value;
NotifyPropertyChanged("UserList");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
Related
I have ObservableCollection of DeviceInformation which is added in MainWindowViewModel and linked with DataContext.
public partial class MainWindow : Window
{
MainWindowViewModel viewModel = new MainWindowViewModel();
public MainWindow()
{
InitializeComponent();
this.DataContext = viewModel;
}
}
Here is the MainWindowViewModel:
public class MainWindowViewModel : ViewModelBase
{
private ObservableCollection<DeviceInformation> allDeviceInfo = new ObservableCollection<DeviceInformation>();
public MainWindowViewModel()
{
// here some of the commands
}
public ObservableCollection<DeviceInformation> AllDeviceInfo
{
get { return allDeviceInfo; }
set
{
allDeviceInfo = value;
this.RaisePropertyChanged(nameof(AllDeviceInfo));
}
}
}
The RaisePropertyChanged is done with implementing ViewModelBase which looks like this:
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected void RaisePropertyChanged(string propertyName)
{
this.RaisePropertyChanged(new PropertyChangedEventArgs(propertyName));
}
protected virtual void RaisePropertyChanged(PropertyChangedEventArgs e)
{
var handler = this.PropertyChanged;
if (handler != null)
{
handler(this, e);
}
}
}
Inside my DeviceInformation I have a List of SyntaxMessages:
public class DeviceInformation : ViewModelBase
{
private List<SyntaxMessages> list = new List<SyntaxMessages>();
private string test = "";
public List<SyntaxMessages> ComConsoleMessages{
get { return list; } // get method
set
{
list = value;
RaisePropertyChanged(nameof(ComConsoleMessages));
} // set method
}
public string Test{
get { return test; } // get method
set
{
test = value;
RaisePropertyChanged(nameof(Test));
} // set method
}
}
This is how the SyntaxMessages looks:
public class SyntaxMessages : ViewModelBase
{
#region private values
private string message = "";
private string status = "";
private string color = "Transparent";
#endregion
#region Public values
public string Message {
get { return message; }
set
{
message = value;
RaisePropertyChanged(nameof(Message));
}
}
public string Status {
get { return status; }
set
{
status = value;
RaisePropertyChanged(nameof(Status));
}
}
public string Color {
get { return color; }
set
{
color = value;
RaisePropertyChanged(nameof(Color));
}
}
#endregion
}
So when I running my program and connecting device to it will collect and add information the the DeviceInformation and this will be added to ObervableCollection of DeviceInformation. This will update my MainTabControl by adding new tab and binding many strings like "Test" (there is more then one) to the TextBoxes, and also update the SubTabControl which is inside the main one. Inside both of the TabItems inside SubTabControl I also have a ListView to which I want link the List of SyntaxMessages this looks like this:
<ListView
Grid.Row="4"
Grid.Column="0"
Grid.ColumnSpan="5"
MinHeight="40"
Padding="0"
Margin="2"
ItemsSource="{Binding ComConsoleMessages, UpdateSourceTrigger=PropertyChanged}">
<ListView.View>
<GridView
AllowsColumnReorder="False">
<GridViewColumn
DisplayMemberBinding="{Binding Message}"
Header="Message" />
<GridViewColumn
Header="Status">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Status}" Foreground="{Binding Color}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
Problem
All works fine except a ListView. When I add some SyntaxMessages to the List of SyntaxMessages called ComConsoleMessages then I have to switch between SubTabControl tabs(SubTabItem1/2) to see ListView updated. I want to update ListView every single time when new message is added to the List of SyntaxMessages which is inside DeviceInfromation which is indside ObservableCollection of DeviceInfromations that is linked via MainWindowViewMode to the window DataContext.
Here is the view:
Ive created a Listbox in Wpf with a Itemtemplate and Datatemplate:
<ListBox Name="LBox" HorizontalContentAlignment="Stretch" Grid.Column="2" SelectionMode="Single">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock x:Name="LTxtBox" Text="{Binding NAME}" Grid.Column="0"/>
<ProgressBar x:Name="PBarLbox" Grid.Column="1" Minimum="0" Maximum="100" Value="{Binding FORTSCHRITT}" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I can add/change/Remove Items in my listbox.
Now ive tried to save the Items in a txt file.
If i Add a item, its work well, i can save them in my txt File.
Now ive tried to save changes in my Listbox, but how can i get access to my Items in a Listbox? .
Here is the Code behind for my Observable List and Property Class.
private ObservableCollection<TodoItem> Todo =new ObservableCollection<TodoItem>();
public class TodoItem : INotifyPropertyChanged
{
public string NAME { get; set; }
public int FORTSCHRITT{ get; set; }
//###########################################
public string Name
{
get { return this.NAME; }
set
{
if (this.NAME != value)
{
this.NAME = value;
this.NotifyPropertyChanged("NAME");
}
}
}
public int fortschritt
{
get { return this.FORTSCHRITT; }
set
{
if (this.FORTSCHRITT != value)
{
this.FORTSCHRITT = value;
this.NotifyPropertyChanged("FORTSCHRITT");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
My thought was, if im gonna to close my Window, Override the old List with the new Changed Items in my Listbox.
For this ive Created a Window close Event:
void DataWindow_Closing(object sender, CancelEventArgs e)
{ // TODO
// Wenn er das Fenster schließt soll er alle Daten die im ToDo Fenster sind speichern und die alte Datei Überschreiben.
}
Ive tried with For each to get access, ive tried for loops but i cant get access.
With:
List<string> list = new List<string>();
string[] arr;
foreach (var x in LBox.Items)
{
list.Add(x.ToString());
}
arr= list.ToArray();
string display=String.Join(Environment.NewLine, arr);
MessageBox.Show(display);
I can see That he got access to the items but he print the following:
enter image description here
How can i print the right values ?
You should not access the control to get the data items. Rather access the source collection directly:
MainViewModel.cs
class MainViewModel : INotifyPropertyChanged
{
public ObservableCollection<TodoItem> TodoItems { get; }
public MainViewModel()
{
this TodoItems = new ObservableCollection<TodoItem>();
}
public async Task SaveDataAsync()
{
var fileContentBuilder = new StringBuilder();
foreach (TodoItem todoItem in TodoItems)
{
fileContentBuilder.AppendLine($"{todoItem.Name}, {todoItem.Fortschritt}");
}
await using var destinationFileStream = File.Open("Destination_File_Path", FileMode.Create);
await using var streamWriter = new StreamWriter(destinationFileStream);
string fileContent = fileContentBuilder.ToString();
await streamWriter.WriteAsync(fileContent);
}
}
MainWindow.xaml.cs
public partial class MainWindow : Window
{
private MainViewModel MainVieModel { get; }
public MainWindow()
{
InitilaizeComponent();
this.MainViewModel = new MainViewModel();
this.DataContext = this.MainViewModel;
this.Closing += SaveDataToFile_OnClosing;
}
private async void SaveDataToFile_OnClosing(object sender, CancelEventArgs e)
=> await this.MainViewModel.SaveDataAsync();
}
MainWindow.xaml
<Window>
<ListBox itemsSource="{Binding TodoItems}">
...
</ListBox>
</Window>
Properties in C# must look like this (pay attention to the proper casing: use camelCase for fields and PascalCase for all other members). Also use nameof to specify the property's name:
private int fortschritt;
public int FortSchritt
{
get => this.fortschritt;
set
{
if (value != this.fortschritt)
{
this.fortschritt = value;
this.NotifyPropertyChanged(nameof(this.Fortschritt));
}
}
}
I have a WPF list view that displays a material, it's thickness , and a unit for the thickess in a combo box...xaml looks like this (I eliminated all the visualization settings for clarity):
<ListView ItemsSource="{Binding Path=MaterialLayers}" IsSynchronizedWithCurrentItem="True">
<ListView.Resources>
<x:Array x:Key="DistanceUnitItems" Type="sys:String" xmlns:sys="clr-namespace:System;assembly=mscorlib">
<sys:String>cm</sys:String>
<sys:String>inches</sys:String>
</x:Array>
<DataTemplate x:Key="ThicknessUnit">
<ComboBox ItemsSource="{StaticResource DistanceUnitItems}" SelectedIndex="{Binding ThicknessUnit}" />
</DataTemplate>
</ListView.Resources>
<ListView.View>
<GridView>
<GridViewColumn Header="Material Name" DisplayMemberBinding="{Binding MaterialName}"/>
<GridViewColumn Header="Material Thickness" DisplayMemberBinding="{Binding MaterialThickness}"/> />
<GridViewColumn Header="Thickness Unit" CellTemplate="{StaticResource ThicknessUnit}" />
</GridView>
</ListView.View>
</ListView>
MaterialLayers is an ObservableCollection<MaterialLayer>
MaterialLayer has properties for MaterialName, MaterialThickness, and ThicknessUnit (which is 0 for cm and 1 for inches). MaterialThickness converts the internally stored value (which is in cm) to the unit specified by ThicknessUnit.
When ThicknessUnit is changed, my DataViewModel calls the PropertyChanged event handler with "MaterialLayers" as the property name.
So, I expected MaterialThickness to update automatically when ThicknessUnit was changed.
I've debugged it, and the PropertyChanged("MaterialLayers") gets called.(When the ThicknessUnit set method is called, it calls an event on the parent data class (MyData), which calls an event on DataViewModel which calls the PropertyChanged handler.)
Relevant code from DataViewModel
public delegate void DataChangedHandler(String identifier);
public DataViewModel()
{
Data = new MyData();
Data.DataChanged += new DataChangedHandler(RaisePropertyChanged);
}
public ObservableCollection<XTRRAMaterialLayer> MaterialLayers
{
get { return _data.MaterialLayers; }
set { }
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propertyName)
{
// take a copy to prevent thread issues
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
Relevant Code from MyData
public class XTRRAData
{
public ObservableCollection<XTRRAMaterialLayer> MaterialLayers { get; set; }
public event DataChangedHandler DataChanged;
public MyData()
{
MaterialLayers = new ObservableCollection<MaterialLayer>();
}
public void myDataChanged(String identifier)
{
DataChanged(identifier);
}
public void AddLayer()
{
MaterialLayer layer = new MaterialLayer() { MaterialName="Test", MaterialThickness=5, ThicknessUnit=0 };
layer.DataChanged += new DataChangedHandler(myDataChanged);
}
}
Relevant Code from MaterialLayer
public class XTRRAMaterialLayer
{
public XTRRAMaterialLayer()
{
_thicknessUnit = 0; // cm
}
public event DataChangedHandler DataChanged;
public String MaterialName { get; set; }
public double MaterialThickness
{
get
{
switch (_thicknessUnit)
{
default:
case 0:
return DIST;
case 1:
return DIST * 0.393700787;
}
}
set
{
switch (_thicknessUnit)
{
default:
case 0:
DIST = value;
break;
case 1:
DIST = value / 0.393700787;
break;
}
}
}
public int _thicknessUnit;
public int ThicknessUnit
{
get { return(int) _thicknessUnit; }
set
{
_thicknessUnit = (eThicknessUnits)value;
FireDataChanged("MaterialLayers");
}
}
public void FireDataChanged(String identifier)
{
if(DataChanged!=null)
{
DataChanged(identifier);
}
}
}
Can anyone help?
You need to implement INotifyPropertyChanged in the class where the property you are changing is - in your case XTRRAMaterialLayer, and raise the PropertyChanged event when the property changes.
Your properties should look something like this:
public int ThicknessUnit
{
get { return(int) _thicknessUnit; }
set
{
_thicknessUnit = (eThicknessUnits)value;
NotifyPropertyChanged();
}
}
The NotifyPropertyChanged event handler:
public event PropertyChangedEventHandler PropertyChanged;
// This method is called by the Set accessor of each property.
// The CallerMemberName attribute that is applied to the optional propertyName
// parameter causes the property name of the caller to be substituted as an argument.
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Okay so the problem is that my INotifyPropertyChanged isnt updting the list view n XAML
DiscoveredData.NetworkedComputersResults = NetworkedComputers; < this is where it loads the data into the DataContext and then calls the iproperty notify changed.
ListView_LocalComputers.ItemsSource = DiscoveredData.NetworkedComputersResults; < using this works fine and i can see all my data however this apparantly not the way to do it.
since i know that i can load the data into the list view using the ItemsSource im thinking the problem is in the XAML.
i would be greatful if someone could point me in the right direction.
Also if you see that i am doing this incorrectly please advise, im fairly new at this coding language and would like to do it the right way
Thank you in advance
<ListView Name="ListView_LocalComputers" ItemsSource="{Binding NetworkedComputerResults}">
<ListView.View>
<GridView>
<GridViewColumn Header="Status">
<GridViewColumn.CellTemplate>
<DataTemplate>
<Border CornerRadius="2,2,2,2" Width="20" Height="20" Background="Transparent" BorderBrush="Transparent" Margin="3,3,3,3">
<Image HorizontalAlignment="Left" VerticalAlignment="Center" Width="12" Height="12" Source="{Binding Image}" Stretch="Fill" Margin="2,2,2,2"/>
</Border>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Local Computers" DisplayMemberBinding="{Binding ComputerName}">
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
//Constructor
public NetworkInformation()
{
InitializeComponent();
this.DataContext = DiscoveredData; //Defines the class to the view
Discovery();
}
//Method
public void Discovery()
{
GetIcon Icon = new GetIcon();
BitmapImage IconOfComputer = null;
List<DiscoveredComputer> NetworkedComputers = new List<DiscoveredComputer>();
DirectoryEntry Discover = new DirectoryEntry("WinNT://Workgroup");
BitmapImage On = Icon.LoadIcon(#"/Images/Icons/ComputerOn.ico");
BitmapImage Off = Icon.LoadIcon(#"/Images/Icons/ComputerOff.ico");
foreach (DirectoryEntry Node in Discover.Children)
{
try
{
if (Node.Properties.Count > 0)
{
IconOfComputer = On;
}
}
catch
{
IconOfComputer = Off;
}
if (Node.Name != "Schema") { NetworkedComputers.Add(new DiscoveredComputer { Image = IconOfComputer, ComputerName = Node.Name, MyToolTip = "Node Type = " + Node.SchemaEntry.Name }); }
}
DiscoveredData.NetworkedComputersResults = NetworkedComputers;
ListView_LocalComputers.ItemsSource = DiscoveredData.NetworkedComputersResults;
}
private class GetIcon
{
public BitmapImage IconStorage { get; set; }
public BitmapImage LoadIcon(String IconPath)
{
BitmapImage GeneratedIcon = new BitmapImage();
GeneratedIcon.BeginInit();
GeneratedIcon.UriSource = new Uri("pack://application:,,," + IconPath, UriKind.RelativeOrAbsolute);
GeneratedIcon.EndInit();
IconStorage = GeneratedIcon;
return GeneratedIcon;
}
}
public class NetworkData : INotifyPropertyChanged
{
#region Property Notify Standard for all classes
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string property)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
#endregion
#region Bound Data To View
private List<DiscoveredComputer> _NetworkedComputersResults;
public List<DiscoveredComputer> NetworkedComputersResults {
get { return _NetworkedComputersResults; }
set
{
_NetworkedComputersResults = value;
NotifyPropertyChanged("NetworkedComputersResults");
}
}
#endregion
public class DiscoveredComputer : NetworkData
{
public string ComputerName { get; set; }
public BitmapImage Image { get; set; }
public String MyToolTip { get; set; }
}
}
You should use ObservableCollection. It implements INotifyCollectionChanged which notifies when a collection changed, not just a single Item.
The items themselves should implement INotifyPropertyChanged of course...
You are binding a plain List<T> to your ListView. However this works fine, it will not fulfill the requirement of dynamic updating the list view when items are added / removed.
If you need the dynamic add/remove elements in your ListView simply use an ObservableCollection<T> instead of the List`.
private ObservableCollection<DiscoveredComputer> _NetworkedComputersResults;
public ObservableCollection<DiscoveredComputer> NetworkedComputersResults {
get { return _NetworkedComputersResults; }
set
{
_NetworkedComputersResults = value;
NotifyPropertyChanged("NetworkedComputersResults");
}
}
If all you need is elements be dynamically be added/removed, then the elements in the observable collection do not need to implement the INotifyPropertyChanged interface.
public class NetworkData
{
public NetworkData()
{
NetworkedComputersResults = new ObservableCollection<DiscoveredComputer>();
}
public ObservableCollection<DiscoveredComputer> NetworkedComputersResults{get;set;}
}
DiscoveryMethod
public void Discovery()
{
GetIcon Icon = new GetIcon();
BitmapImage IconOfComputer = null;
List<DiscoveredComputer> NetworkedComputers = new List<DiscoveredComputer>();
DirectoryEntry Discover = new DirectoryEntry("WinNT://Workgroup");
BitmapImage On = Icon.LoadIcon(#"/Images/Icons/ComputerOn.ico");
BitmapImage Off = Icon.LoadIcon(#"/Images/Icons/ComputerOff.ico");
foreach (DirectoryEntry Node in Discover.Children)
{
try
{
if (Node.Properties.Count > 0)
{
IconOfComputer = On;
}
}
catch
{
IconOfComputer = Off;
}
if (Node.Name != "Schema") { NetworkedComputers.Add(new DiscoveredComputer { Image = IconOfComputer, ComputerName = Node.Name, MyToolTip = "Node Type = " + Node.SchemaEntry.Name }); }
}
//Use Clear and Add .Dont assign new instance DiscoveredData.NetworkedComputersResults=new ....
DiscoveredData.NetworkedComputersResults.Clear();
foreach (var item in NetworkedComputers)
{
DiscoveredData.NetworkedComputersResults.Add(item);
}
}
I hope this will help.From my personal View it would be good If you create this Discovery method in ViewModel and would Call it from the Constructor of ViewModel . It seems two way communication like you setting property of ViewModel from View thats the job of Binding not code behind
In WPF, I have a ListView of 2 columns and the first column needs to be a button. Correct me if I'm wrong, but the only way I found to implement a button in a ListView is to use a DataTemplate. The problem I found with this is I have no way to maintain my original button Properties when they are mapped with a DataTemplate so I am forced to use binding to remap every individual property (including custom Properties since I'm actually using a custom User Control which inherits from Button). This seems extraneous to have to manually map all Properties so maybe there's a better way to automatically persist those properties?
Here's my test code:
public MainWindow() {
InitializeComponent();
ObservableCollection<ScreenRequest> screenRequests = new ObservableCollection<ScreenRequest>() {
new ScreenRequest("A", "1"),
new ScreenRequest("B", "2")
};
myListView.ItemsSource = screenRequests;
}
public class ScreenRequest {
public CustomButton ScreenButton { set; get; }
public string Details { set; get; }
public ScreenRequest(string buttonText, string customProperty) {
this.ScreenButton = new CustomButton();
this.ScreenButton.Content = buttonText;
this.ScreenButton.CustomProperty = customProperty;
this.ScreenButton.Click += new RoutedEventHandler(InitiateScreenRequest);
}
private void InitiateScreenRequest(object sender, RoutedEventArgs e) {
CustomButton screenBtn = (CustomButton)sender;
screenBtn.Content = "BUTTON TEXT CHANGED";
}
}
public class CustomButton : Button {
public string CustomProperty { get; set; }
}
And the XAML:
<Window...
...
<Window.Resources>
<DataTemplate x:Key="ButtonTemplate">
<local:CustomButton Content="{Binding ScreenButton.Content}"/>
</DataTemplate>
</Window.Resources>
<Grid x:Name="grdMain">
...
<ListView...
<ListView.View>
<GridView x:Name="gridView">
<GridViewColumn CellTemplate="{StaticResource ButtonTemplate}" Width="Auto" Header="Screen" HeaderStringFormat="Screen"/>
<GridViewColumn Header="Details" HeaderStringFormat="Details" DisplayMemberBinding="{Binding Details}"/>
</GridView>
</ListView.View>
</ListView>
</Grid>
</Window>
So my questions are:
Do I have to manually map every single property in the CustomButton in order for it to carry over to the DataTemplate or is their a catch-all to automatically persist the Properties?
How do I map the CustomProperty Property in the binding such that it sticks with the button? Do I use a DependencyProperty for this?
How do I maintain my click event such that clicking the button in GridView will call the InitiateScreenRequest function? Ideally I'd like to have a single method declared for all buttons, but I haven't gotten to that point yet.
Any help or insight into buttons in listviews would be appreciated.
<Window x:Class="MiscSamples.TonyRush"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="TonyRush" Height="300" Width="300">
<ListView ItemsSource="{Binding}">
<ListView.View>
<GridView>
<GridViewColumn Width="Auto" Header="Screen" HeaderStringFormat="Screen">
<GridViewColumn.CellTemplate>
<DataTemplate>
<Button Command="{Binding SomeAction}" Content="{Binding ActionDescription}" Width="100"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Details" HeaderStringFormat="Details" DisplayMemberBinding="{Binding Details}" Width="100"/>
</GridView>
</ListView.View>
</ListView>
</Window>
Code Behind:
public partial class TonyRush : Window
{
public TonyRush()
{
InitializeComponent();
DataContext = new List<ScreenRequest>
{
new ScreenRequest() {ActionDescription = "Click Me!"},
new ScreenRequest() {ActionDescription = "Click Me Too!"},
new ScreenRequest() {ActionDescription = "Click Me Again!!"},
};
}
}
ViewModel:
public class ScreenRequest: INotifyPropertyChanged
{
public Command SomeAction { get; set; }
private string _actionDescription;
public string ActionDescription
{
get { return _actionDescription; }
set
{
_actionDescription = value;
NotifyPropertyChanged("ActionDescription");
}
}
private string _details;
public string Details
{
get { return _details; }
set
{
_details = value;
NotifyPropertyChanged("Details");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public ScreenRequest()
{
SomeAction = new Command(ExecuteSomeAction) {IsEnabled = true};
}
//public SomeProperty YourProperty { get; set; }
private void ExecuteSomeAction()
{
//Place your custom logic here based on YourProperty
ActionDescription = "Clicked!!";
Details = "Some Details";
}
}
Key part: The Command class:
//Dead-simple implementation of ICommand
//Serves as an abstraction of Actions performed by the user via interaction with the UI (for instance, Button Click)
public class Command : ICommand
{
public Action Action { get; set; }
public void Execute(object parameter)
{
if (Action != null)
Action();
}
public bool CanExecute(object parameter)
{
return IsEnabled;
}
private bool _isEnabled;
public bool IsEnabled
{
get { return _isEnabled; }
set
{
_isEnabled = value;
if (CanExecuteChanged != null)
CanExecuteChanged(this, EventArgs.Empty);
}
}
public event EventHandler CanExecuteChanged;
public Command(Action action)
{
Action = action;
}
}
Result:
Notes:
Take a look at how separate UI is from Data and functionality. This is the WPF way. Never mix UI with data / business code.
The Command in the ViewModel serves as an abstraction for the Button. The ViewModel doesn't know what a Button is, nor should it. Let me know if you need further details.