Help please.
I am updating an application that needs to display alarm data dynamically in a ListView.
Currently when data arrives the alarm condition is evaluated and displayed graphically
in XAML controls using DependencyProperties in Custom UserControls and INotifyPropertyChanged
events in the MainViewModel. This works fine to change the color of the fields
as required with the updates pulled through live.
My task is to implement some alarm logic such as user acknowledgment of active
alarms etc. To this end I'm writing the alarm details to a LocalDB instance and
displaying them in a ListView page. This page populates ok when I load up the
dB with a _Loaded event when I navigate to the page however when on the page I
can't seem to get the data to update live using the same method that updates the
graphic colors!
I have created class to hold LV-style string data that populates from the Db so
there shouldn't be an issue with that. I don't want to post too much code,
but can anyone tell me if I should be populating each field of the ListView with
individual data bindings or can I rely a data binding that can be passed the whole
LV alarm class?
public class LVAlarmItem
{
public string Name { get; set; }
public string Status { get; set; }
public string Raised { get; set; }
public string Id { get; set; }
public string Ack { get; set; }
}
public ObservableCollection<Alarm> AlarmList = new ObservableCollection<Alarm>(); //Real alarms from Db
public ObservableCollection<LVAlarmItem> LVAlarmItems = new ObservableCollection<LVAlarmItem>();
// LV items populated from Db
foreach (Alarm alarm in _viewModel.AlarmManager.AlarmList)
{
lVAlarmItems.Add(new AlarmManager.LVAlarmItem() { Name = alarm.AlarmName, Status = alarm.ValueStatus.ToString(), Raised = alarm.RaisedTimeStamp.ToString(), Id = alarm.Id.ToString(), Ack = alarm.Ack.ToString() } );
}
AlarmList.Alarm_ListView.ItemsSource = lVAlarmItems;
This XAML works for the _loaded event:
<ListView.View>
<GridView>
<GridViewColumn Header="Name" Width="300" DisplayMemberBinding="{Binding Name}" />
<GridViewColumn Header="Status" Width="300" DisplayMemberBinding="{Binding Status}" />
<GridViewColumn Header="Raised" Width="300" DisplayMemberBinding="{Binding Raised}" />
<GridViewColumn Header="Id" Width="250" DisplayMemberBinding="{Binding Id}" />
<GridViewColumn Header="Acknowledged" Width="200" DisplayMemberBinding="{Binding Ack}" />
</GridView>
</ListView.View>
It feels like my LV should be populated directly with Alarms to be honest, but I thought I'd try to get it working with the simpler structure first. New to MVC which doesn't help, hope my question makes sense.
More info: I'm trying to bind my main XAML page to the user control:
<Controls:AlarmList x:Name="AlarmList" MainAlarmList="{Binding MainAlarmList}" Width="1350" Margin="60,63,100,100" Background="{x:Null}" Loaded="AlarmList_Loaded" Height="600" />
where in the cs of the user control I have is :
{
public static readonly DependencyProperty MainAlarmListProperty = DependencyProperty.Register(
"MainAlarmList", typeof(ObservableCollection<AlarmManager.LVAlarmItem>), typeof(AlarmList), new PropertyMetadata(OnPropertyChanged));
public void Render()
{
}
public ObservableCollection<AlarmManager.LVAlarmItem> MainAlarmList
{
get { return (ObservableCollection<AlarmManager.LVAlarmItem>)GetValue(MainAlarmListProperty); }
set { SetValue(MainAlarmListProperty, value); }
}
Screenshot:Link
Related
I currently have a Listview box with 3 comboboxes. I am populating them with from a sql database. For each row, I want to have the 3rd combobox change it's contents based on the selected values of the 2nd combobox.
The comboboxes will be: cmbx1 (employee[jack, jill, tom, lisa]), cmbx2(products[pen, pencil, stapler]), cmbx3(color - will be dynamic based on what color is available for the product)
product and color options: pen[red, blue, black]; pencil[black, orange, red]; stapler[pink, teal, purple, brown]
If for Row1, the user selects a pen, then only the available colors for that product will be listed in the color combobox for that row. The next row could have a different color option based on the product selected.
Is this possible or should i find another way to achieve the results?
here's what a currently have...
<ListView.View>
<GridView>
<GridViewColumn Header="Employee" Width="150">
<GridViewColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding lStrEmployee}" Width="120" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Product" Width="150">
<GridViewColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding lStrProduct}" Width="120" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Color" Width="150">
<GridViewColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding lStrColor}" Width="120" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</ListView.View>
code behind
List<Int32> liEmployee = new List<Int32>();
List<string> lsEmployee = new List<string>();
List<Int32> liProduct = new List<Int32>();
List<string> lsProduct = new List<string>();
List<Int32> liColor = new List<Int32>();
List<string> lsColor = new List<string>();
SqlConnection conn = new SqlConnection("Data Source=localhost\\SQLEXPRESS;Initial Catalog=testDB;Persist Security Info=True;User ID=USER;Password=PASSWORD;");//Connect Timeout=900
SqlCommand cmd1 = new SqlCommand("select id,employee from testDB.dbo.dmEmployee where inactive=0", conn);
SqlCommand cmd2 = new SqlCommand("select id,Product from testDB.dbo.tblProductList where inactive=0", conn);
SqlCommand cmd3 = new SqlCommand("select id,Color from testDB.dbo.Color where inactive=0", conn);
conn.Open();
SqlDataReader dr1 = cmd1.ExecuteReader();
while (dr1.Read())
{
liEmployee.Add(dr1.GetInt32(dr1.GetOrdinal("id")));
lsEmployee.Add(dr1.GetString(dr1.GetOrdinal("employee")));
}
conn.Close();
conn.Open();
SqlDataReader dr2 = cmd2.ExecuteReader();
while (dr2.Read())
{
liProduct.Add(dr2.GetInt32(dr2.GetOrdinal("id")));
lsProduct.Add(dr2.GetString(dr2.GetOrdinal("Product")));
}
conn.Close();
conn.Open();
SqlDataReader dr3 = cmd3.ExecuteReader();
while (dr3.Read())
{
liColor.Add(dr3.GetInt32(dr3.GetOrdinal("id")));
lsColor.Add(dr3.GetString(dr3.GetOrdinal("Color")));
}
conn.Close();
List<lvItem> itemFound = new List<lvItem>();
itemFound.Clear();
lvItem puzzlePieces;
for (int cnt = 0; cnt < 10; cnt++)
{
puzzlePieces = new lvItem();
puzzlePieces.lStrEmployee = lsEmployee;
puzzlePieces.lStrDatabase = lsDatabase;
puzzlePieces.lStrProvider = lsProvider;
itemFound.Add(puzzlePieces);
}
list1.ItemsSource = itemFound;
Thanks!
I'm surprised that you didn't get any answers to your question. Maybe it's because you don't seem to be doing things the WPF way, or maybe because you're asking for so much?
First things first... you need to create a data type class that implements the INotifyPropertyChanged interface and contains all of the properties required for display in each row of the ListView. In your case, you need three collections and three selected item values. As an example, you could do something like this (implementing the INotifyPropertyChanged interface yourself):
public class RowData : INotifyPropertyChanged
{
public ObservableCollection<Employee> Employees { get; set; }
public Employee SelectedEmployee { get; set; }
public ObservableCollection<Product> Products { get; set; }
public Product SelectedProduct { get; set; }
public ObservableCollection<Brush> Colours { get; set; }
public Brush SelectedColour { get; set; }
}
Note the use of the Brush class rather than the Color struct, this is because Brush is a class, which means that we can bind to it and also because it is more predominantly used in WPF.
However, it is not optimal having the same collections in every object in every row, except for the Colours collection, which could be different for each row. Having said that, that is exactly what I'm going to do because it will be quicker for me to explain and you can improve your code yourself at a later stage:
So now you have your data type class, we need to add a property of that type to bind to your ListView control. If you are using the code behind of your MainWindow, then let's create a DependencyProperty for it:
public static readonly DependencyProperty RowDataProperty = DependencyProperty.
Register("RowData", typeof(ObservableCollection<RowData>), typeof(MainWindow),
new UIPropertyMetadata(new ObservableCollection<RowData>()));
public ObservableCollection<RowData> RowData
{
get { return (ObservableCollection<RowData>)GetValue(RowDataProperty); }
set { SetValue(RowDataProperty, value); }
}
After filling your collection, you can now bind it to the ListView control:
xmlns:Local="clr-namespace:YourWpfApplicationName"
...
<ListView ItemsSource="{Binding RowData, RelativeSource={RelativeSource AncestorType={
x:Type Local:MainWindow}}}">
...
</ListView>
In short, the RelativeSource Binding is simply looking for the property you defined in the code behind. Now, how to define that a ComboBox should appear in each GridViewColumn? You need to define the GridViewColumn.CellTemplate:
<GridViewColumn Header="Employees">
<GridViewColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding Employees}" SelectedItem="{Binding
SelectedEmployee}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
You'll need to define the other columns from this example. So the final part of this puzzle is how to update the content of the Colours ComboBox dependent on the selected values of the other ComboBoxes? The answer lies in your selected value properties in your RowData class:
public Employee SelectedEmployee
{
get { return selectedEmployee; }
set
{
selectedEmployee = value;
NotifyPropertyChanged(SelectedEmployee);
Colours = GetColours();
}
}
private ObservableCollection<Brush> GetColours()
{
ObservableCollection<Brush> newColours = new ObservableCollection<Brush>();
if (SelectedEmployee.Name == "Some Name" && SelectedProduct.Name ==
"Some Product") newColours.AddRange( new List<Brush>() { Brushes.Red,
Brushes.White, Brushes.Blue } );
else ...
}
There are many ways to do this and I'll leave that up to you. You should now have a working example and I now realise why nobody had answered your question... far too much for anyone sane to type! After spending so long on this, I would appreciate it if you try to solve any minor problems you find with this on your own and hope it helps you.
I'm building a MVVM application with WPF that uses a number of relatively complex list views. I've adopted the pattern that the collection that the list view binds to is a collection of View-Model objects, rather than a list of the underlying model objects - I've done this by databinding to an entirely separate column that is populated with code that looks a bit like this
var itemsSource = messages.Select(i => new MessageViewModel(i));
In this case the list view is displaying a list of Message objects to the user. This works OK, however is fairly clunky - expecially when dealing with collection change events.
Now I want to re-use this ListView elsewhere in my application to display a different list of messages to the user in a consistent way - the options that I can see are
Create a list view that derives from ListView and data binds to a collection of type MessageViewModel
Create a control that databinds to a collection of Message objects that either contains or derives from a list view data bound to some internally constructed collection of MessageViewModel
The first option requires that everyone who uses the control run the clunky code that builds and maintains the MessageViewModel collection, the second option encapsulates the maintenance of this view model collection however means that I need to re-implement any member of ListView which exposes the underlying items in the collection so that they can be converted back to the original Message type.
I have a number of similar list views that have similar re-usability problems.
Is there a better approach to dealing with WPF ItemsControl based views that allows for these views to be re-used in an MVVM application?
It looks to me that there are two things you want to reuse:
Exposing a collection of MessageViewModel, so you can bind this collection to the itemsSource of the ListView.
(Optionally), you have a style (or content presenter, or data template) on your specific list view, which you want to reuse. This part may also include code behind, triggers, etc.
You should not mixed the two.
#2 can be achieved with a style which you'll apply to the list view, or a data template. Personally, I like to define a dedicated class as a collection of MessageViewModel, and in your data template set the TargetType to be that class.
#1 is a class that implements Collection, INotifyCollecitonChanged, and INotifyPropertyChanged. Best way (and easiest) would be to merely a wrap it around ObservableCollection. In construction, do the Select method. Then have method for the book keeping.
Below some sample (working!) code. Note there is no code behind for the view. I put the two lists twice in the grid. The usage of ContentControl and DataTemplate is my style - there are dozens of other ways to do it.
======= Model.cs ====
using System;
namespace SO
{
class Message
{
public string from { get; set; }
public string to { get; set; }
public string subject { get; set; }
public DateTime received { get; set; }
}
}
======= ViewModel.cs ====
using System;
using System.Collections.Generic;
using System.Linq;
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace SO
{
class MessageVM : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private Message m_model;
public MessageVM( Message model ) {
m_model = model;
}
private void raize( string prop ) {
PropertyChanged( this, new PropertyChangedEventArgs("prop") );
}
public string from {
get { return m_model.from; }
set { m_model.from = value; raize("from"); }
}
public string to {
get { return m_model.to; }
set { m_model.subject = value; raize("to") ); }
}
public string subject {
get { return m_model.subject; }
set { m_model.subject = value; raize("subject") ); }
}
public DateTime received {
get { return m_model.received; }
set { m_model.received = value; raize("recieved") ); }
}
}
class FolderVM : ObservableCollection<MessageVM>
{
public FolderVM( IEnumerable<Message> models )
:base( models.Select( msg => new MessageVM(msg) ) )
{
}
}
class SampleData
{
//static public FolderVM folder { get; set; }
static public FolderVM folder;
static SampleData( )
{
// create a sample model
List<Message> model = new List<Message>();
model.Add( new Message { from = "Bill", to = "Steve", subject = "Resusable Items Control", received = DateTime.Now.AddDays(-4) } );
model.Add( new Message { from = "Steve", to = "Bill", subject = "Resusable Items Control", received = DateTime.Now.AddDays( -3 ) } );
model.Add( new Message { from = "Bill", to = "Jeff", subject = "stack", received = DateTime.Now.AddDays( -2 ) } );
// initialize the view model
folder = new FolderVM( model );
}
}
}
======= MainWindow.xaml ====
<Window x:Class="Paf.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:src="clr-namespace:SO"
Title="MainWindow" Height="350" Width="525"
>
<Window.Resources>
<DataTemplate DataType="{x:Type src:FolderVM}">
<ListView ItemsSource="{Binding}">
<ListView.View>
<GridView>
<GridViewColumn Header="from" Width="80" DisplayMemberBinding="{Binding Path=from}" />
<GridViewColumn Header="to" Width="80" DisplayMemberBinding="{Binding Path=to}" />
<GridViewColumn Header="subject" Width="200" DisplayMemberBinding="{Binding Path=subject}" />
<GridViewColumn Header="received" Width="160" DisplayMemberBinding="{Binding Path=received}" />
</GridView>
</ListView.View>
</ListView>
</DataTemplate>
</Window.Resources>
<StackPanel>
<ContentControl Content="{Binding Source={x:Static src:SampleData.folder}}" />
<ContentControl Content="{Binding Source={x:Static src:SampleData.folder}}" />
</StackPanel>
</Window>
How to toggle scroll lock in a WPF ListView?
When more items are added to a ListView, than there is space to show the following should happen depending of the scroll lock state.
When scroll lock is enabled the ListView should not scroll when adding more items (this is the default behavior).
When scroll lock is disabled the ListView should automatically scroll to the bottom so the newly added items are visible to the user.
The scroll lock state should be controlled by the (seldom used) 'scroll lock' button on a typical keyboard.
EDIT: A bit of code...
<ListView x:Name="logMessagesListView" ItemsSource="{Binding ElementName=self, Path=LogMessages}">
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn Header="Created" Width="100" DisplayMemberBinding="{Binding Created}"/>
<GridViewColumn Header="Level" Width="80" DisplayMemberBinding="{Binding LogLevel}"/>
<GridViewColumn Header="Message" Width="350" DisplayMemberBinding="{Binding Message}"/>
</GridView.Columns>
</GridView>
</ListView.View>
</ListView>
I would keep the log messages in an ObservableCollection, both for automatic UI notifications and the CollectionChanged event. Once a new item is added, check if the button is clicked. If it is, move to the last item (or you can use the index/item properties of the event arguments).
You're going to need to add System.Windows.Forms to the project references so you can check the button state.
public partial class MainWindow : Window
{
private ObservableCollection<LogMessage> logMessages;
public MainWindow()
{
this.logMessages = new ObservableCollection<LogMessage>();
/* add/load some data */
this.logMessages.CollectionChanged += new NotifyCollectionChangedEventHandler(this.LogMessages_CollectionChanged);
this.LogMessages = CollectionViewSource.GetDefaultView(this.logMessages);
InitializeComponent();
}
public ICollectionView LogMessages
{
get;
set;
}
private void LogMessages_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
if (System.Windows.Forms.Control.IsKeyLocked(System.Windows.Forms.Keys.Scroll))
{
this.LogMessages.MoveCurrentToLast();
}
}
}
}
public class LogMessage
{
public string Created
{ get; set; }
public string LogLevel
{ get; set; }
public string Message
{ get; set; }
}
Put ScrollViewer.CanContentScroll="False" in your XAML and that should work!
Why don't I see the details in this example. I cannot change the structure of dataclass, master class and detail class. So I have to solve this with the correct binding.
public class ViewModel
{
public dataclass data { get; set; }
public ViewModel()
{
data = new dataclass();
master a_master = new master();
a_master.mastername = "hello";
detail a_detail = new detail();
a_detail.detailname = "goodbye";
data.details.Add(a_detail);
data.Add(a_master);
}
}
public class dataclass : ObservableCollection<master>
{
public ObservableCollection<detail> details { get; set; }
public dataclass()
{
details = new ObservableCollection<detail>();
}
}
public class master
{
public string mastername { get; set; }
}
public class detail
{
public string detailname { get; set; }
}
And in my XAML I am binding like this:
<Window x:Class="md.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:md.viewmodels"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<vm:ViewModel/>
</Window.DataContext>
<StackPanel Orientation="Vertical" >
<ListView ItemsSource="{Binding Path=data}">
<ListView.View>
<GridView>
<GridViewColumn Header="master" DisplayMemberBinding="{Binding mastername}"/>
</GridView>
</ListView.View>
</ListView>
<ListView ItemsSource="{Binding Path=data/details}">
<ListView.View>
<GridView>
<GridViewColumn Header="detail" DisplayMemberBinding="{Binding detailname}"/>
</GridView>
</ListView.View>
</ListView>
</StackPanel>
</Window>
Try
<ItemsSource="{Binding Path=data.details}">
instead of
<ItemsSource="{Binding Path=data/details}">
I think what you tried to achieve was kind of a master/detail scenario with binding to hierarchical data like decribed in How to: Use the Master-Detail Pattern with Hierarchical Data. In fact, as long as you have an ObservableCollection<details> as property of a class derived from ObservableCollection<master> this is not hierarchical, and hence the / in the binding expression won't work. See PropertyPath XAML Syntax, section Source Traversal (Binding to Hierarchies of Collections) for details about the /.
Also there are widely accepted conventions for capitalization in C#, saying that you should use Pascal casing for public types like the classes and properties here.
Im usign a Ribbon Window and in the "content area beneath" I have a grid in which I will be displaying UserControls. To demonstrate my problem lets take a look at this simple UserControl:
<ListView x:Name="lvPersonList">
<ListView.View>
<GridView>
<GridViewColumn Width="120" Header="Name" DisplayMemberBinding="{Binding Name}"/>
<GridViewColumn Width="120" Header="Height" DisplayMemberBinding="{Binding Height}"/>
</GridView>
</ListView.View>
</ListView>
And the code:
public partial class MyUserControl: UserControl
{
private List<Person> personList;
public TestSnpList()
{
InitializeComponent();
this.personList = new List<Person>();
this.personList.Add(new Person { Name = "Chuck Norris", Height = 210 });
this.personList.Add(new Person { Name = "John Rambo", Height = 200 });
this.lvPersonList.ItemsSource = personList;
}
}
public class Person
{
public string Name { get; set; }
public int Height { get; set; }
}
The parent Window:
<Grid x:Name="grdContent" DockPanel.Dock="Top">
<controls:MyUserControl x:Name="myUserControl" Visibility="Visible"/>
</Grid>
I don't understant why this binding doesn't work. Instead of values (Name and Height) I get full class names. If I use this code in a Window it works fine.
Any ideas? I would like this user contorl works for itself (it gets the data form the DB and represents it in a ListView)...
Thanks!
It seems the problem is with RibbonWindow.
If I use Window and UserControl binding works fine, but if I use RibbonWindow (Odyssey Ribbon) binding doesn't work. What I don't understand is that in design mode I can see proper values and in running mode I see only class names:
http://i977.photobucket.com/albums/ae255/HekoSLO/designModeVSrunning.png