I'm creating a logger application thing to learn WPF, and I want new messages to appear at the bottom of a scrollviewer every time some listener picks up a new message. I want each message to create something that looks like this:
==================================
= Source | Message =
= | =
= | =
==================================
I've got 2 WPF books here, and they both have "warnings" saying that there are other simpler and better mechanisms than custom/user controls in most cases. I was wondering if this can be solved using control templates, and if so, what control type should I use as a base?
Perhaps Binding can help. (It might overcomplicate things if you're still learning WPF though.)
With binding we can make the ListView only be the visible representation of your logging, meaning the logic to add/remove logs can stay separate from the ListView entirely.
class LogEntry
{
public string Source { get; set; }
public string Message { get; set; }
public LogEntry(string source, string message)
{
Source = source;
Message = message;
}
}
class Listener
{
private int m_maxLogs = 10;
private ObservableCollection<LogEntry> m_logEntries;
public ObservableCollection<LogEntry> LogEntries { get { return m_logEntries; } }
public Listener()
{
m_logEntries = new ObservableCollection<LogEntry>();
}
public void AddLogEntry(string source, string message)
{
if (LogEntries.Count >= m_maxLogs)
{
//Remove the oldest LogEntry.
LogEntries.RemoveAt(0);
}
LogEntries.Add(new LogEntry(source, message));
}
}
If the datacontext is set to the Listener instance the xaml becomes (based on the previous answer):
<ListView ItemsSource="{Binding LogEntries}">
<ListView.View>
<GridView>
<GridViewColumn Header="Source" Width="120" DisplayMemberBinding="{Binding Source}"/>
<GridViewColumn Header="Message" Width="400" DisplayMemberBinding="{Binding Message}"/>
</GridView>
</ListView.View>
</ListView>
If you want to dynamically change the text of your log entries for some reason, you'll need to implement the INotifyPropertyChanged interface in the LogEntry class to make the listview update.
Try using using a ListView and setting its View to a GridView.
<ListView>
<ListView.View>
<GridView>
<GridViewColumn Header="Source" Width="120" />
<GridViewColumn Header="Message" Width="400" />
</GridView>
</ListView.View>
</ListView>
Refer: How to: Display ListView Contents by Using a GridView
Related
I have a list of items I wish to display in a ListView/GridView. Each item is a class/object containing a format and array of bytes. The format dictates how the bytes are to be displayed (hex or decimal). I am using a converter to go back and forth between TextBox.Text and the byte array.
The converter needs the format and string/array. I tried to use IValueConverter and pass the format as a ConverterParameter, but this didn't work since it is not a DependencyProperty. I tried to use IMultiValueConverter but that didn't work because I do not get the format in ConvertBack. I thought that if I could bind to the whole object (MyDataItem), then the converter would work fine. However, I cannot figure out how to bind to that. I tried a bunch of variations using RelativeSource and other properties, but couldn't figure it out. Can someone help me with the binding?
If there is a better way to accomplish my task, feel free to suggest it.
public enum FormatEnum
{
Decimal,
Hex
}
public class MyDataItem
{
public byte[] Data { get; set; }
public FormatEnum Format { get; set; }
}
public class ViewModel
{
ObservableCollection<MyDataItem> DataItems = new ObservableCollection<MyDataItem>();
}
XAML (with non-working binding)
<ListView ItemsSource="{Binding DataItems}">
<ListView.View>
<GridView>
<GridViewColumn Header="Format">
<GridViewColumn.CellTemplate>
<DataTemplate>
<!--ComboBox for the format-->
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Data">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding Path,
Converter={StaticResource ResourceKey=DataBytesConverter},
ConverterParameter={Binding Format}}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
Welcome to SO!
Just a heads-up for future....you'll have a much better chance of getting questions answered if you provide an MCVE. The more work you make people do to reproduce your exact problem, the less inclined they'll be to do so.
There are a couple of different ways of doing what you're trying to do, but the main problem is that the data you are binding to doesn't support INPC. I'll let you read up on that, but the problem is essentially that the left hand (your text display field) doesn't know what your right hand (the ComboBox) is doing. When the ComboBox changes the format it has to signal to anything that depends on it that the value has changed. That's where INPC comes in. You can implement it yourself if you like, and there are plenty of tutorials around the net showing how to do so, but it's much easier to just use an existing library like MVVM Light, which is what I'll use here.
Before I go into detail, I should point out that your idea of using an IMultiValueConverter would in fact work, provided you passed in Data and Format as separate parameters so that it would update whenever either of them changes value. Many people will actually suggest solutions like this, but it's not the best way of doing what you're trying to achieve. Converters and behaviors are really just extensions of the view, which is fine when they expose parts of the view that you otherwise can't get to. But in your case the problem is that the data you are providing the view isn't in a format that your view can readily consume, and in a "proper" WPF application, it should be. Fixing your data beforehand is usually faster, a lot easier to debug when it goes wrong, and it opens up the possibility for things like unit testing. We already know that your MyDataItem class has to be either modified or replaced in order to support INPC, so that's a good place to do your values-to-text logic.
So to start with, create a view model for your model object that exposes the properties you want to pass to your view laye (i.e. "Format") and adds a new one for the text string you want to display (i.e. "DataText). We'll also create an update function to fill that string in, INPC support and a tiny bit of update logic:
public class MyDataItemViewModel : ViewModelBase
{
private MyDataItem DataItem;
public MyDataItemViewModel(MyDataItem dataItem)
{
this.DataItem = dataItem;
UpdateDataText();
}
public FormatEnum Format
{
get { return this.DataItem.Format; }
set
{
if (this.DataItem.Format != value)
{
this.DataItem.Format = value;
RaisePropertyChanged(() => this.Format);
UpdateDataText();
}
}
}
private string _DataText;
public string DataText
{
get { return this._DataText; }
set
{
if (this._DataText != value)
{
this._DataText = value;
RaisePropertyChanged(() => this.DataText);
}
}
}
private void UpdateDataText()
{
switch (this.Format)
{
case FormatEnum.Decimal:
this.DataText = String.Join(", ", this.DataItem.Data.Select(val => String.Format("{0:D}", val)));
break;
case FormatEnum.Hex:
this.DataText = String.Join(", ", this.DataItem.Data.Select(val => String.Format("0x{0:X2}", val)));
break;
default:
this.DataText = String.Empty;
break;
}
}
}
Your "DataItems" collection needs to be public and accessible via a getter (something you didn't do in your original code), and it needs to be a collection of these view models instead:
public ObservableCollection<MyDataItemViewModel> DataItems { get; } = new ObservableCollection<MyDataItemViewModel>();
And now for each instance of MyDataItem you want to wrap it in an instance of MyDataItemViewModel:
this.DataItems.Add(new MyDataItemViewModel(new MyDataItem { Format = FormatEnum.Decimal, Data = new byte[] { 1, 2, 3 } }));
this.DataItems.Add(new MyDataItemViewModel(new MyDataItem { Format = FormatEnum.Decimal, Data = new byte[] { 4, 5, 6 } }));
Now your data is in a much better format. ObjectDataProvider provides a nice and convenient way to put all the values for a particular type of Enum in a single list that a ComboBox can bind to, so let's create one of those for your FormatEnum:
xmlns:system="clr-namespace:System;assembly=mscorlib"
<Window.Resources>
<ObjectDataProvider x:Key="FormatEnumValues" MethodName="GetValues" ObjectType="{x:Type system:Enum}">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="local:FormatEnum"/>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
</Window.Resources>
And your ListView can now bind directly to all this data without any converters or behaviors or anything messy like that:
<ListView ItemsSource="{Binding DataItems}">
<ListView.View>
<GridView>
<GridViewColumn Header="Format">
<GridViewColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding Source={StaticResource FormatEnumValues}}" SelectedItem="{Binding Path=Format}" Width="100" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Data">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding DataText}" TextTrimming="CharacterEllipsis" MinWidth="100" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
Result:
I'm facing a Collection was modified; enumeration operation may not execute error in a WPF application under Framework 4.0 in VS2010.
This collection error only happens when I'm launching the compiled debug version of the program. Problem is I'm unable to trace the error as no intellisence informations are given, and I'm not able to find which line of code this error is about.
What would you do to reach the erroned piece of code? Is there an option to toggle to enable intellisence in debug .exe? I have a message telling me to compile in x86, but this is already compiled in x86 mode.
Source
Not sure which part of the code I should paste, but it gives an idea about the 'pattern' of the application.
This is my ListView, binded to a List of < AudioFile >
<ListView AllowDrop="True" Drop="dropMP3" DragEnter="dropMP3Begin"
GridViewColumnHeader.Click="GridViewColumnHeaderClickedHandler"
ScrollViewer.VerticalScrollBarVisibility="Visible"
ScrollViewer.HorizontalScrollBarVisibility="Disabled" UseLayoutRounding="False"
ItemsSource="{Binding}" x:Name="myTracks" Margin="0" AlternationCount="2"
Foreground="#FFEFEFEF" FontSize="13.333" VerticalAlignment="Top"
SizeChanged="myTracks_SizeChanged" Background="Black"
MouseDoubleClick="myTracksDoubleClick">
These are my columns:
<GridViewColumn Width="Auto" DisplayMemberBinding="{Binding Artist}">
<TextBlock Text="Artist"></TextBlock>
</GridViewColumn>
<GridViewColumn Width="Auto" Header="Title" DisplayMemberBinding="{Binding Title}"/>
<GridViewColumn Width="Auto" Header="Album" DisplayMemberBinding="{Binding Album}"/>
<GridViewColumn Width="Auto" DisplayMemberBinding="{Binding Length}">
<Image Source="Icons\clock.png" Height="15"/>
</GridViewColumn>
When application starts
myTracks.DataContext = songs;
This is my object
public class AudioFile
{
public String Artist { get; set; }
public String Title { get; set; }
public String Album { get; set; }
public String fileLocation { get; set; }
public String Length { get; set; }
public String Track { get; set; }
}
When a song is found:
void lm_SongFound(AudioFile file)
{
songs.Add(file);
}
That usually happens when you modify the collection during a foreach iteration.
For example:
foreach(var item in items){
items.Remove(item);
}
As you can see, the collection is being modified while it is being iterated.
The fact that you don't see the exception may not mean it doesn't exist. Maybe it's being trapped. This used to happen with WPF bindings.
Is this possible?
I have a ListView and I want to create a template for the columns, so that each column that I mark as a 'cellTextBox' displays with a textbox in it (and also calls the TextBox_LostFocus() on the LostFocus event). I'd really like to use a single template rather than defining a DockPanel and TextBox for every single column. However, each column is going to be bound to a different column in the data source.
In other words, I'd like a "cookie cutter" for GridViewColumns that allows me to specify a different binding path for each one.
I tried something like this (among other things), but it doesn't work.
Any ideas?
<Page.Resources>
<DataTemplate x:Key="cellTextBox">
<DockPanel>
<TextBox
LostFocus="TextBox_LostFocus"
Width="100"
Text="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},Path=DisplayMemberBinding}"
/>
</DockPanel>
</DataTemplate>
</Page.Resources>
<StackPanel>
<ListView ScrollViewer.HorizontalScrollBarVisibility="Disabled"
HorizontalAlignment="Stretch"
ItemsSource="{Binding Tables[0]}"
Width="Auto"
x:Name="Service_Tasks">
<ListView.View>
<GridView>
<GridViewColumn Width="120" CellTemplate="{StaticResource cellTextBox}" DisplayMemberBinding={Binding Path=Field1} Header="Field1" />
<GridViewColumn Width="120" CellTemplate="{StaticResource cellTextBox}" DisplayMemberBinding={Binding Path=Field2} Header="Field2" />
<GridViewColumn Width="120" CellTemplate="{StaticResource cellTextBox}" DisplayMemberBinding={Binding Path=Field3} Header="Field3" />
<GridViewColumn Width="120" CellTemplate="{StaticResource cellTextBox}" DisplayMemberBinding={Binding Path=FieldN} Header="FieldN" />
<!-- ... other columns ... -->
</GridView>
</ListView.View>
</ListView>
</StackPanel>
EDIT - I thought I would share the Visual Basic translation of #H.B.'s solution, which worked for me. Perhaps this will help out someone else.
Imports System.Windows.Markup
Public Class TemplateBuilderExtension : Inherits StaticExtension
Dim _path As String
Shared _tagpath As String
'Private TagPath As String
Shared Property TagPath As String
Get
TagPath = _tagpath
End Get
Private Set(ByVal value As String)
_tagpath = value
End Set
End Property
Property Path As String
Get
Path = _path
End Get
Set(ByVal value As String)
_path = value
End Set
End Property
Public Overrides Function ProvideValue(ByVal serviceProvider As System.IServiceProvider) As Object
TagPath = Path
Dim resourceExt = New StaticResourceExtension("TemplateBuilder_BaseTemplate")
'Dim baseTemplate As DataTemplate = DirectCast(resourceExt.ProvideValue(serviceProvider), DataTemplate)
ProvideValue = DirectCast(resourceExt.ProvideValue(serviceProvider), DataTemplate)
End Function
Public Sub New()
MyBase.new()
End Sub
Public Sub New(ByVal Path As String)
Me.Path = Path
End Sub
End Class
Public Class TemplateBuilderTagExtension : Inherits MarkupExtension
Public Sub New()
MyBase.New()
End Sub
Public Overrides Function ProvideValue(ByVal serviceProvider As System.IServiceProvider) As Object
ProvideValue = New Binding(TemplateBuilderExtension.TagPath)
End Function
End Class
Obviously, the XAML is the same, regardless.
Do not forget to add the following namespace reference to your root-level tag:
xmlns:me="clr-namespace:WpfApplication1", replacing WpfApplication with whatever your application's root namespace is (this can be found in your application's properties).
Thanks again #H.B.!
The only method i can think of right now to inline this is using markup-extensions as you try to pass more than one value to one property.
Getting variable data into the template seems to be non-trivial though and the following approach using a static property is quite the hack, maybe you can think of something better:
<ListView ItemsSource="{Binding Data}">
<ListView.Resources>
<!-- x:Shared="False" because otherwise the Tag extension is only evaluated the first time the resource is accessed -->
<DataTemplate x:Shared="false" x:Key="TemplateBuilder_BaseTemplate">
<TextBox Text="{me:TemplateBuilderTag}" Width="100" LostFocus="TextBox_LostFocus" />
</DataTemplate>
</ListView.Resources>
<ListView.View>
<GridView>
<GridViewColumn CellTemplate="{me:TemplateBuilder Name}" />
<GridViewColumn CellTemplate="{me:TemplateBuilder Occupation}" />
</GridView>
</ListView.View>
</ListView>
public class TemplateBuilderExtension : MarkupExtension
{
public string Path { get; set; }
public TemplateBuilderExtension() { }
public TemplateBuilderExtension(string path)
{
Path = path;
}
// Here be dirty hack.
internal static string TagPath { get; private set; }
public override object ProvideValue(IServiceProvider serviceProvider)
{
TagPath = Path;
var resourceExt = new StaticResourceExtension("TemplateBuilder_BaseTemplate");
// This line causes the evaluation of the Tag as the resource is loaded.
var baseTemplate = resourceExt.ProvideValue(serviceProvider) as DataTemplate;
return baseTemplate;
}
}
public class TemplateBuilderTagExtension : MarkupExtension
{
public override object ProvideValue(IServiceProvider serviceProvider)
{
return new Binding(TemplateBuilderExtension.TagPath);
}
}
I tried to use a custom service provider when getting the resource but unfortunately it did not arrive in ProvideValue of the tag, that would have been a better way to get the path accross.
You could of course create the template dynamically from scratch in the TemplateBuilderExtension, that way you will not run into such issues.
HI
I have this code
private void Button_Click(object sender, RoutedEventArgs e)
{
ObservableCollection<ListProduct> myList = new ObservableCollection<ListProduct>();
int index = myList.IndexOf((from parcour in myList
where parcour.name == myProduct.name
select parcour).FirstOrDefault());
if(index != -1)
{
myList[index].number++;
}
}
public class ListProduct
{
public string name { get; set; }
public int number{ get; set; }
}
XAML:
<ListView Name="ListView12" ItemsSource="{Binding}" Height="201">
<ListView.View>
<GridView>
<GridViewColumn Width="100" Header="name"
DisplayMemberBinding="{Binding name}" />
<GridViewColumn Width="100" Header="number"
DisplayMemberBinding="{Binding nombre}" />
</GridView>
</ListView.View>
</ListView>
this snippet is to modify the number of occrnce of an element on myListView
when I click the button and made those change myListView do not change. Any Help please -- is something missing?
If your code is exactly as you show it, this is because you are creating a new ObservableCollection which is local to the Button_Click method, and letting it go out of scope. You need to either set the DataContext to the new collection, or (more idiomatic) modify the elements of the collection which is your existing DataContext.
If you adopt the first option, you'll need to fix the code, because the LINQ query is running over the new (empty) collection and will always return null.
If you're already doing the second option, or change you code to use the second option, you still have a problem because ListProduct is not raising property change notification. So when you modify the number property, WPF has no way of detecting this change and updating the binding.
To fix this, implement INotifyPropertyChanged on the ListProduct class, and update your property setters to raise the PropertyChanged event. (You will not be able to use C# automatic properties; you must implement the property getters and setters by hand.)
I am trying to populate a datagrid (or gridview) as a child elment of a treeview from the database. I am able to get data from the DB in the tree, however, it doesnt seem to work for the datagrid. Here is my xaml code:
<Window x:Class="AttemptUsingHirarchichalData.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:data="clr-namespace:AttemptUsingHirarchichalData"
xmlns:my="http://schemas.microsoft.com/wpf/2008/toolkit"
Title="Window1" Height="300" Width="300">
<Window.Resources>
<HierarchicalDataTemplate DataType="{x:Type data:Root}"
ItemsSource="{Binding Path=RootList}">
<TextBlock Text="{Binding RootNode}"/>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type data:Nodes}"
ItemsSource="{Binding Path=ChildList}">
<TextBlock Text="{Binding ChildNode}"/>
</HierarchicalDataTemplate>
</Window.Resources>
<Grid>
<TreeView Name="TreeView1">
<TreeViewItem ItemsSource="{Binding Path=RootList}"
Header="{Binding RootNode}"/>
<TreeViewItem ItemsSource="{Binding Path=dt_Age}"
Header="{Binding dt_Age}"/>
</TreeView>
</Grid>
My codebehind is something like this:
InitializeComponent();
Root obj_Root = new Root();
obj_Root.RootNode = "RootNode";
obj_Root.RootList = new List<Nodes>();
Class1 obj_Class1 = new Class1();
DataTable dt_Age = obj_Class1.GetAgeInComboBox();
for (int i = 0; i < dt_Age.Rows.Count; i++)
{
Nodes obj_AgeNode = new Nodes();
obj_AgeNode.ChildNode = dt_Age.Rows[i][0].ToString();
obj_Root.RootList.Add(obj_AgeNode);
Class1 obj_class = new Class1();
DataTable dt_name = new DataTable();
dt_name = obj_class.GetName(Convert.ToInt32(dt_Age.Rows[i][0]));
obj_AgeNode.ChildList = new List<Nodes>();
//gridv
for (int j = 0; j < dt_name.Rows.Count; j++)
{
Nodes obj_NameNode = new Nodes();
obj_NameNode.ChildNode = dt_name.Rows[j][0].ToString();
obj_AgeNode.ChildList.Add(obj_NameNode);
}
}
TreeView1.DataContext = obj_Root;
My Class file has this as a part of it:
public class Nodes
{
public string ChildNode { get; set; }
public List<Nodes> ChildList { get; set; }
}
public class Root
{
public string RootNode { get; set; }
public List<Nodes> RootList { get; set; }
}
public DataTable GetAgeInComboBox()
{
SqlDataAdapter da_Age = new SqlDataAdapter("select distinct Age from myfrstattemt", conn1);
DataTable dt_Age = new DataTable();
da_Age.Fill(dt_Age);
return dt_Age;
}
Please tell me how to implement it. I am new to this, so please excuse my stupid errors, and please try to explain in simple terms. Thank you.
This is what I actually need to do
The good news is that you're doing a lot more work here than you need to, which is probably why you're having trouble.
The bad news is that you should really study a little more about WPF to properly grok this and come up with a good approach that's clean and concise. I'll try and point you in the right direction.
Firstly, you should get your head around ItemsControl. It's a really powerful class and is the base class of many of the everyday controls you would use in a WPF application. You should understand how binding any collection (IEnumerable, IList, IBindingList etc) to the ItemsSource property of ItemsControl will cause child items to be created.
You should then understand (if you don't already) how data types are converted into UI elements via DataTemplates. This is a simple but powerful concept.
Then you should experiment with a small extension to the above, namely HeaderedItemsControl and HierarchicalDataTemplate. This will give you all the tools you need to use the TreeView in the way you want to.
At no point would you need to create any TreeViewItems in C# code. If you can get the underlying data objects to reflect the hierarchy you want to display (irrespective of whether each node is a simple text label or a data grid) then you can create hierarchical data templates for all levels and have WPF take care of binding everything and creating the TreeViewItems for you.
EDIT
I have some questions for your edited question:
What is the difference between Root and Nodes?
Do you have a class hierarchy that models the relationship between nodes? If so, just use that rather than copying the objects into instances of Root and Nodes. I'll give you a made up example.
Let's assume you have Customers who place Orders, and each order has Items.
public class Customer
{
public string Name { get; set; }
public IEnumerable<Order> Orders { get; set; }
}
public class Order
{
public DateTime PurchaseDate { get; set; }
public IEnumerable<OrderItem> Items { get; set; }
}
public class OrderItem
{
public string ProductName { get; set; }
public int Quantity { get; set; }
public double UnitPrice { get; set; }
public double TotalPrice { get; set; }
}
The above types represent a hierarchy. If you have a structure like this, then you can bind it directly to the UI. You don't need to create any Root or Node objects. This is the WPF way :)
(Note that if you don't have the above class hierarchy, you might set about creating one specifically for use in the UI. Read more about the MVVM pattern if you're interested.)
In your XAML you would define the TreeView as:
<TreeView x:Name="_treeView" ItemsSource="{Binding}">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type data:Customer}"
ItemsSource="{Binding Path=Orders}">
<TextBlock Text="{Binding Name}"/>
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type data:Order}">
<StackPanel>
<TextBlock Text="{Binding PurchaseDate}"/>
<ListView ItemsSource="{Binding Items}">
<ListView.View>
<GridView>
<GridViewColumn DisplayMemberBinding="{Binding ProductName}" />
<GridViewColumn DisplayMemberBinding="{Binding Quantity}" />
<GridViewColumn DisplayMemberBinding="{Binding UnitPrice}" />
<GridViewColumn DisplayMemberBinding="{Binding TotalPrice}" />
</GridView>
</ListView.View>
</ListView>
</StackPanel>
</DataTemplate>
</TreeView.Resources>
</TreeView>
And in code-behind, you'd do something like this:
_treeView.DataContext = customers; // eg. IEnumerable<Customer>
Might be worth having a look at this post by Marlon Grech.