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!
Related
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
I'm trying to implement a custom GridView for the ListView, but I need to find a way to get the ListView that is associated with the GridView, since I need to access some of the ListView properties (Width, Items, Template...).
I found an old post that was asking the same question Get the parent listview from a gridview object but it never got an answer...
If anyone has an idea, I would be glad :)
EDIT: Here some basic code from the custom GridView
public class GridViewEx : GridView
{
public ListView Owner {get; set;} // This is what I need to get
public GridViewEx()
{
}
}
EDIT2: I found another solution than the one presented by mm8. Since I also needed a custom GridViewHeaderRowPresenter, which is used in the ListView Scrollviewer Style, here is what I came up with (as for now):
public class GridViewHeaderRowPresenterEx : GridViewHeaderRowPresenter
{
private GridViewEx _GridView;
public GridViewHeaderRowPresenterEx()
{
Loaded += OnLoaded;
Unloaded += OnUnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
if (this.GetVisualParent<ListView>() is ListView lv && lv.View is GridViewEx gridView)
{
_GridView = gridView;
_GridView.Owner = lv;
}
}
private void OnUnLoaded(object sender, RoutedEventArgs e)
{
if (_GridView != null)
_GridView.Owner = null;
}
}
And here is the extension method to get the ListView from the custom GridViewHeaderRowPresenter:
public static class DependencyObjectExtensions
{
public static T GetVisualParent<T>(this DependencyObject depObj) where T : DependencyObject
{
if (VisualTreeHelper.GetParent(depObj) is DependencyObject parent)
{
var result = (parent as T) ?? GetVisualParent<T>(parent);
if (result != null)
return result;
}
return null;
}
}
The GridViewHeaderRowPresenter Loaded event is called when a GridView is added to a ListView, and the Unloaded event is called when the GridView is removed from the ListView.
I prefer this solution over the one from mm8, since it required (if I'm not mistaken) the ListView to have Items in order to work.
Thanks for the suggestions :)
You could override the PrepareItem method and use the ItemsControl.ItemsControlFromItemContainer method to get a reference to the parent ListView:
public class GridViewEx : GridView
{
public ListView Owner { get; set; }
public GridViewEx()
{
}
protected override void PrepareItem(ListViewItem item)
{
base.PrepareItem(item);
Owner = Owner ?? ItemsControl.ItemsControlFromItemContainer(item) as ListView;
}
}
You can use Binding with RelativeSource and AncestorType, try the code below:
XAML:
<Grid>
<ListView ItemsSource="{Binding}">
<ListView.View>
<GridView>
<GridViewColumn Header="Name" Width="120" DisplayMemberBinding="{Binding Name}" />
<GridViewColumn Header="Age" Width="50" DisplayMemberBinding="{Binding Age}" />
<GridViewColumn Header="Mail" Width="150" DisplayMemberBinding="{Binding Mail}" />
<GridViewColumn Header="Test">
<GridViewColumn.CellTemplate>
<DataTemplate>
<Button
DataContext="{Binding Path=ItemsSource, RelativeSource={RelativeSource AncestorType={x:Type ListView}}}"
Content="Get ListView ItemsSource Count"
Click="Test_Click" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
</Grid>
C#:
class User
{
public string Name { get; set; }
public int Age { get; set; }
public string Mail { get; set; }
}
public MainWindow()
{
InitializeComponent();
DataContext = new List<User>
{
new User() { Name = "John Doe", Age = 42, Mail = "john#doe-family.com" },
new User() { Name = "Jane Doe", Age = 39, Mail = "jane#doe-family.com" },
new User() { Name = "Sammy Doe", Age = 7, Mail = "sammy.doe#gmail.com" }
};
}
private void Test_Click(object sender, RoutedEventArgs e)
{
if (sender is FrameworkElement fe)
if (fe.DataContext is List<User> users)
MessageBox.Show($"ItemsSource items count: {users.Count()}.", "ListView Test");
}
I am looking for a way to know if my listview contains a value. Below is my code.
public class OnlineUserList
{
public string Name { get; set; }
public string Color { get; set; }
}
<ListView x:Name="lvOnlineUsers" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" SelectionMode="Single" SelectionChanged="lvOnlineUsers_SelectionChanged">
<ListView.View>
<GridView x:Name="lvOnlineUsersGridView" AllowsColumnReorder="False">
<GridViewColumn Header="Online Users" Block.TextAlignment="Center" TextOptions.TextFormattingMode="Display" TextBlock.FontWeight="Bold">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Name="tbOnlineUsersGridView" Text="{Binding Path=Name}" Foreground="{Binding Path=Color}" HorizontalAlignment="Center" VerticalAlignment="Center" TextOptions.TextFormattingMode="Display" Style="{StaticResource ResourceKey=lblLabel}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
public void AddUserToList(string username)
{
lvOnlineUsers.Items.Add(new OnlineUserList { Name = username, Color = "Black" });
}
Now this is where am having issue
public void RemoveUserFromList(string username)
{
if(lvOnlineUsers.Items.Contains(username))
lvOnlineUsers.Items.Remove(username);
}
You should learn MVVM.
In the mean time, put the items in an ObservableCollection and assign it to the listview's ItemsSource property in your codebehind. Thereafter, repeat after me: Never, ever touch lvOnlineUsers.Items. Never, never, never. Forget that it exists. Everything you do, you interact with the ObservableCollection. Search it, add items to it, remove items from it. The UI will magically and mysteriously update itself.
I'm going to assume this is in MainWindow. If this is in a different view, the constructor will have a different name.
public MainWindow()
{
InitializeComponent();
lvOnlineUsers.ItemsSource = _onlineUsers;
}
private ObservableCollection<OnlineUserList> _onlineUsers
= new ObservableCollection<OnlineUserList>();
public void AddUserToList(string username)
{
_onlineUsers.Add(new OnlineUserList { Name = username, Color = "Black" });
}
public void RemoveUserFromList(string username)
{
// We don't search _onlineUsers for the string username, because
// _onlineUsers doesn't contain strings. It contains your user class.
// So instead, we look for the user class instance that has the name
// we want.
var found = _onlineUsers.FirstOrDefault(ou => ou.Name == username);
if (found != null)
{
_onlineUsers.Remove(found);
}
}
Until you have looked into MVVM, try this:
for(int i = lvOnlineUsers.Items.Count - 1; i >= 0; --i)
{
OnlineUserList item = lvOnlineUsers.Items[i] as OnlineUserList;
if (item != null && item.Name == username)
lvOnlineUsers.Items.Remove(lvOnlineUsers.Items[i]);
}
I'm displaying a list of items in a WPF ListView, the items have a Quantity, Order Code and a Description. The columns are bound to fields in an ObservableCollection held in the View Model. This is all very standard and works as would expect. However, in the Quantity Column of the ListView I am adding two button + and -, the idea being that when they are pressed the value of the quantity either increments or decrements. The problem is that because these buttons are not bound to a field in the ObservableCollection I cannot get a link from the button being pressed in the List View to the record in the ObservableCollection. I have tried getting the item selected in the ListView but it is the button that gets selected when pressed and not the ListView item, I have also captured the item beneath the mouse pointer when the button is pressed but it could be pressed using the keyboard.
I feel there must be a (simple!) way of doing this but I can't find it.
This is the XAML:
<ListViewName="AccessoriesContent" >
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn Header="Select">
<GridViewColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Name="QuantityStack">
<Button Name="SubtractAccessoryButton" Command="vx:DataCommands.SubtractAccessory" Content="-" />
<TextBox Name="QuantityTextBox" Text="{Binding Quantity, Mode=TwoWay}" />
<Button Name="AddAccessoryButton" Command="vx:DataCommands.AddAccessory" Content="+" />
</StackPanel>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Order Code" DisplayMemberBinding="{Binding OrderCode}" />
<GridViewColumn Header="Description" DisplayMemberBinding="{Binding Description}" />
</GridView.Columns>
</GridView>
</ListView.View>
</ListView>
The code behind:
public MainWindow()
{
//CommandBindings.Add(
InitializeComponent();
AccessoryVM = new AccessoryViewModel();
AccessoriesContent.ItemsSource = AccessoryVM.AccessoryCollection;
}
And the ViewModel:
class AccessoryViewModel
{
ObservableCollection<AccessoryData> _AccessoryCollection =
new ObservableCollection<AccessoryData>();
public ObservableCollection<AccessoryData> AccessoryCollection
{ get { return _AccessoryCollection; } }
public void PopulateAccessories(string order_code)
{
// Read the data and populate AccessoryCollection
}
}
public class AccessoryData : INotifyPropertyChanged
{
private int _quantity;
public int Quantity
{
get { return _quantity; }
set
{
this._quantity = value;
Notify("Quantity");
}
}
public string OrderCode { get; set; }
public string Description { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
protected void Notify(string propName)
{
if (this.PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
}
Beyond this I have two methods SubtractAccessory and AddAccessory which are triggered by the buttons but I have yet to populate them with anything that would work.
Another option is to create a RelayCommand (see here). In this model you create an ICommand property on each of your items. You then set this property to a new RelayCommand that accepts a delegate you would like to be ran when that command is activated. So this could be a QuantityUp method and a QuantityDown method on your AccessoryData. Once you've got your ICommand property in place you simply bind to it like this, where QuantityUpCommand is your ICommand property.
<GridViewColumn Header="" >
<GridViewColumn.CellTemplate>
<DataTemplate>
<Button Height="15" Width="15" Content="+" Command="{Binding QuantityUpCommand}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
The AccessoryData would look something like this
private RelayCommand _quantityUpCommand;
public ICommand QuantityUpCommand
{
get
{
if (_quantityUpCommand == null)
{
_quantityUpCommand = new RelayCommand(QuantityUp);
}
return _quantityUpCommand;
}
}
public void QuantityUp(object obj)
{
Quantity++;
}
And RelayCommand looks something like this:
public class RelayCommand: ICommand
{
#region Fields
readonly Action<object> _execute;
readonly Predicate<object> _canExecute;
#endregion // Fields
#region Constructors
public RelayCommand(Action<object> execute)
: this(execute, null)
{
}
public RelayCommand(Action<object> execute, Predicate<object> canExecute)
{
if (execute == null)
throw new ArgumentNullException("execute");
_execute = execute;
_canExecute = canExecute;
}
#endregion // Constructors
#region ICommand Members
public bool CanExecute(object parameter)
{
return _canExecute == null ? true : _canExecute(parameter);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void Execute(object parameter)
{
_execute(parameter);
}
#endregion // ICommand Members
}
You do not appear to have posted the commands you use.
Anyway, if you do use commands you could either use instance commands which exist on the view model (you then will need to bind the command to the command property on the DataContext) and hence have access to the Quantity or you can pass the view model as CommandParameter just setting it to {Binding}, then in the command you can cast the parameter to the VM and change the Quantity.
(If you were to use the Click event you could just cast the sender to Button and cast its DataContext to the VM)
You could pass the current item through a CommandParameter on the button that uniquely identifies the current item. So that in the execution of the Command you know what item you're talking about. If you can't find a unique token in your item, you could even pass the whole item!
<Button Name="AddAccessoryButton" Command="vx:DataCommands.AddAccessory" CommandParameter="{Binding}" Content="+" />
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