I've been struggling for a while with getting a listbox to update in WPF. I have a user control that includes a listbox. I then embed this usercontrol within another user control.
public partial class MyClass : UserControl
{
private Thread myThread;
public MyClass()
{
InitializeComponent();
this.DataContext = this;
}
ObservableCollection<String> sampleData = new ObservableCollection<String>();
public ObservableCollection<String> SampleData
{
get
{
if (sampleData.Count <= 0)
{
sampleData.Add("1");
sampleData.Add("2");
}
return sampleData;
}
}
public void StartMyThread()
{
if (myThread == null)
{
myThread = new Thread(ThreadProgram);
myThread.Start();
}
}
private void UpdateFromThread(string text)
{
this.Dispatcher.Invoke((Action)(() =>
{
UpdateList(text);
}));
}
private void UpdateList(string text)
{
sampleData.Add(text);
}
private void ThreadProgram()
{
UpdateFromThread("my text");
//other stuff
}
My XAML for the user control that contains the list box contains:
<Grid>
<ListBox ItemsSource="{Binding Path=SampleData}"/>
</Grid>
and the place where I embed my control is here:
<TabControl SelectionChanged="TabControl_SelectionChanged" Name="tabCtrl">
<TabItem Header="MyListboxTab">
<Service:MyClass Margin="0,0,-1,0"/>
</TabItem>
It updates initially with the "1" and "2" and I can use a messagebox to display the text instead. I've checked and my updates to the listbox are definitely being called, they just don't display.
It's also worth noting that the list updates when calling sampleData.Add("clicked"); from a button click.
Any help is much appreciated, thanks.
Related
I have two windows: (1) Home and (2) Add
What I want to do is to update the DataGrid on Home whenever I hit the Submit button on the Add page.
The data loaded in the DataGrid is a CSV File. I'm hoping to accomplish this without the use of MVVM if that is possible.
So far, the code that I have works but only on the first run. For example, I fill up the fields in Add then hit the Submit button for the first time. The DataGrid would update but when I try it for a second time (without closing the application), it would no longer do what I want it to do. How can I resolve this?
Submit button click event in Add
public void btn_Submit_Click(object sender, RoutedEventArgs e)
{
save();
string edata = #"Employees.txt";
Home h = new Home();
h.grid.ItemsSource = edata.ToList();
btn_clear_Click(sender, e);
error_mssg.Text = "** SUCCESS! **";
}
Home page xaml
<StackPanel Margin="0 10 0 0">
<DataGrid x:Name="grid" Height="400" ItemsSource="{Binding}"/>
</StackPanel>
Home page cs
public void Populate()
{
DataTable table = LoadDataGrid.display(#"EmployeeData.txt");
grid.DataContext = table;
}
LoadDataGrid cs
public static DataTable display(string path)
{
DataTable dt = new DataTable();
using (StreamReader sr = new StreamReader(path))
{
string[] headers = sr.ReadLine().Split(',');
foreach (string header in headers)
{
dt.Columns.Add(header);
}
while (!sr.EndOfStream)
{
string[] rows = sr.ReadLine().Split(',');
DataRow dr = dt.NewRow();
for (int i = 0; i < headers.Length; i++)
{
dr[i] = rows[i];
}
dt.Rows.Add(dr);
}
}
return dt;
}
I also added the code I used to populate the DataGrid with the contents of the csv file in case it is needed.
I also tried doing
h.grid.Items.Refresh();
instead of
h.grid.ItemsSource = edata.ToList();
I do it this way:
public class Home {
private AddPage ap;
public void createAddPage() {
ap = new AddPage();
ap.setHome(this);
}
public DataGrid getDataGrid() {
return your_datagrid;
}
}
public class AddPage {
private Home home;
public void setHome(Home home) {
this.home = home;
}
public void button_click(/*some params*/) {
DataGrid dg = home.getDataGrid();
//do stuff with dg...
}
}
i have created a simple solution that i think solves your problem. I simplified it to a case where your "Home" window displays a list of names. In the second window, a user can type in a new name that is then added to the list of names displayed in the first window.
Approach:
The solution was to add an event to the second window. When the "Home" window creates an instance of the second window, the home window subscribes to the second window's event. When the second window fires the event, the second window passes the new name to be added to the list displayed on the first window.
So, adding to the list is done in the "Home" window.
Note: The list of items being displayed is actually an ObservableCollection<string>.
Here are the two windows:
Here's the custom EventArgs object that i use to pass the details of the event, in this case, the new name just-added in the second window:
public class NameAddedEventArgs:EventArgs
{
public string NewName { get; set; }
}
Here's the code in my "Home" window. The window is simply name "MainWindow":
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ObservableCollection<string>();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
var window1 = new Window1();
window1.ItemAddedEvent += ItemAddedEventHandler;
window1.Show();
}
private void ItemAddedEventHandler(object sender, NameAddedEventArgs e)
{
(DataContext as ObservableCollection<string>).Add(e.NewName);
}
}
So as you can see, when i create "Window1", which is the second window, i subscribe to its event.
Here's the code in Window1:
public partial class Window1 : Window
{
public event EventHandler<NameAddedEventArgs> ItemAddedEvent;
public Window1()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
RaiseNewNameEvent(txtInput.Text);
}
protected void RaiseNewNameEvent(string newName)
{
var nameAddedEventArgs = new NameAddedEventArgs { NewName=newName };
var handler = ItemAddedEvent;
if (handler != null)
{
handler(this, nameAddedEventArgs);
}
}
}
Here, when the button is clicked to add the name (the name is typed into txtInput), i raise the event, passing in the new name typed-in.
currently I'm working on a chat client and changed my client from windows forms (as forms is shitty) to WPF. I'm not really sure which control could be used to realize a chatbox. I could take a TextBox but this will not display the complete content when it's full. I also tried to use a ListBox but when I try to add items they are not displayed. I used this code to add content to it:
internal void AddMessage(string message)
{
listBox_messages.Items.Add(message);
listBox_messages.Items.Refresh();
}
Does anybody knows which control would be the best for this purpose?
Thanks for your help!
Edit:
I implemented a TextBox for this and disabled it. But the text I'm appending by this method is not shown.
My class:
using System.ComponentModel;
using System.Windows;
namespace Chat_Client
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
/// <summary>
/// MainWindow constructor
/// </summary>
public MainWindow()
{
InitializeComponent();
textBox_messages.AppendText("Test" + "\n");
textBox_messages.AppendText("Test" + "\n");
textBox_messages.AppendText("Test" + "\n");
Closing += OnWindowClosing;
}
private void OnWindowClosing(object sender, CancelEventArgs e)
{
Program.Shutdown();
}
private void button_connect_Click(object sender, RoutedEventArgs e)
{
if(Program.Connected)
{
Program.Disconnect();
}
else
{
Program.Connect();
}
}
private void button_sendMessage_Click(object sender, RoutedEventArgs e)
{
}
internal void AddMessage(string message)
{
textBox_messages.AppendText(message + "\n");
}
}
}
The test strings are shown but the text added by the method AddMessage is not. I can verify that the method is called, I just checked it with a breakpoint inside this method. Anybody has a clue how this could happen?
Update
If the Program class calls AddMessage(string) from within another thread than the UI thread, you have to use a Dispatcher in order to update the UI.
Application.Current.Dispatcher.BeginInvoke(new Action(() =>
{
mainWindow.AddMessage(message);
}));
MVVM is the way to go when using WPF.
Add your messages to an ObservableCollection in your ViewModel class (ChatViewModel.cs):
public class ChatViewModel
{
public ObservableCollection<string> Messages { get; } = new ObservableCollection<string>();
internal void AddMessage(string message)
{
Messages.Add(message);
}
}
Set this ViewModel as the DataContext of your View (ChatView.xaml)
public ChatView : UserControl
{
public ChatView()
{
InitializeComponent();
DataContext = new ChatViewModel();
}
}
In the XAML-Code bind the ObservableCollection to the ItemsSource property of your ListBox:
<ListBox ItemsSource="{Binding Messages}") />
When you add a message to the Messages collection it should appear inside the ListBox. This is not a complete example, but it should guide you into the right direction.
*In response to your edit: where are you calling AddMessage() from? I tried your code and just called AddMessage("foo"); from the button click event and worked fine.
For a chat box, I would use a TextBox to write the chat messages, and wrap it with a ScrollViewer for scrolling. After using AppendText() on the TextBox to write the message, you can then call ScrollToEnd() to scroll to the bottom of the TextBox.
In the XAML:
<ScrollViewer x:Name="ScrollViewer" ScrollChanged="ScrollViewer_OnScrollChanged">
<TextBox x:Name="ChatBox"/>
</ScrollViewer>
In the code behind:
private void WriteToChat(string message)
{
ChatBox.AppendText(message);
ChatBox.ScrollToEnd();
}
private bool _autoScroll = true;
private void ScrollViewer_OnScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (e.ExtentHeightChange == 0)
{
_autoScroll = ScrollViewer.VerticalOffset == ScrollViewer.ScrollableHeight;
}
if (_autoScroll && e.ExtentHeightChange != 0)
{
ScrollViewer.ScrollToVerticalOffset(ScrollViewer.ExtentHeight);
}
}
You can use a TextBox and set the VerticalAlignment to Stretch in your Xaml file.
< TextBox x:Name="textbox" VerticalAlignment="Stretch">
I'm trying to make some sort of wheel spinning. I have 5 customized text blocks, text file with the list of values (it may consist of 1-1000 items). After reading the file I have a 'List fileValues' with its values. I decided to create another 'List wheel' which will contain up to 5 elements at the time and is expected to be bind to text blocks.
When one presses a spin button, last element of 'wheel' is removed and new element from 'values' is added to the beginning of the 'wheel' list.
In order UI will be responsive to changes in the list, it is good to bind each element in the 'wheel' to corresponding text block on UI. But what I tried to do up to this moment didn't work.
Here is what I tried to do (the code is a little bit dirty, but I try to make it work firstly).
5 customized text blocks
<TextBlock Name="Value1" TextWrapping="WrapWithOverflow"/>
<TextBlock Name="Value2" TextWrapping="WrapWithOverflow"/>
<TextBlock Name="Value3" TextWrapping="WrapWithOverflow"/>
<TextBlock Name="Value4" TextWrapping="WrapWithOverflow"/>
<TextBlock Name="Value5" TextWrapping="WrapWithOverflow"/>
ObservableList which implements INotifyCollectionChanged interface
class ObservableList : INotifyCollectionChanged, IEnumerable
{
private readonly List<string> _valuesList;
public string First
{
get { return _valuesList.First(); }
}
public string Last
{
get { return _valuesList.Last(); }
}
public ObservableList()
{
this._valuesList = new List<string>();
}
public string this[Int32 index]
{
get
{
if (_valuesList.Count == 0 || index + 1 > _valuesList.Count)
{
return "------";
}
return _valuesList[index];
}
}
public void AddLast(string value)
{
_valuesList.Add(value);
OnNotifyCollectionChanged();
}
public void AddFirst(string value)
{
_valuesList.Insert(0, value);
OnNotifyCollectionChanged();
}
public void RemoveFirst()
{
_valuesList.RemoveAt(0);
OnNotifyCollectionChanged();
}
public void RemoveLast()
{
_valuesList.Remove(_valuesList.Last());
OnNotifyCollectionChanged();
}
public event NotifyCollectionChangedEventHandler CollectionChanged;
public void OnNotifyCollectionChanged()
{
if (CollectionChanged != null)
{
CollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
public IEnumerator GetEnumerator()
{
return (_valuesList as IEnumerable).GetEnumerator();
}
}
XAML Code-behind
public partial class MainWindow : Window
{
private List<string> _values = new List<string>();
private ObservableList _uiValues = new ObservableList();
public MainWindow()
{
InitializeComponent();
Value1.DataContext = _uiValues[0];
Value2.DataContext = _uiValues[1];
Value3.DataContext = _uiValues[2];
Value4.DataContext = _uiValues[3];
Value5.DataContext = _uiValues[4];
}
private void LoadFileBtn_Click(object sender, RoutedEventArgs e)
{
//Loads text file and fills _values
}
private void SpinBtn_Click(object sender, RoutedEventArgs e)
{
InitUiTextBlocks();
//Spin simulation
}
private void InitUiTextBlocks()
{
_uiValues.Clear();
for (int i = 0; i < 5; ++i)
{
//Nothing appears on UI and CollectionChanged event is null
_uiValues.AddLast(_values.First());
_values.RemoveAt(0);
}
}
}
I tried to use 'ObservableCollection', but the effect is the same. Nothing appears on UI. In fact I can't imagine how to bind each of List element to specific Label. Is it even possible to do such binding?
In the XAML do something like:
<Label Name="some_name" Content="{Binding SomeStingProperty}"/>
and in the code behind, have a
public string SomeStringProperty {get; set;}
you can bind to a collection as well, and if it's an ObservableCollection it will update on change.
search for basic XAML binding otherwise :)
(on a side note, it's cleaner i think it the XAML, i personally don't like to do it in the code behind ...)
As a side note, and totally self promoting, here are 2 articles that will probably help:
Understanding selected value
The big mvvm template.
The second might be a bit over your head if you're a beginner, but should be worth reading nevertheless.
I have observable collection called (Users) in view model that binded with ListViewControl (lstUsers) in view and what I need is to scroll to current logged in user in List View .
I see in most of examples that used scroll from code behind as following e.g. :
lstUsers.ScrollIntoView(lstUsers[5]);
but what I need is to handle it from view model .
Please advice !
One way of doing this would be to use something like an ICollectionView which has a current item. You can then set IsSynchronizedWithCurrentItem to true to link the current item in the view model to the selected item in the ListView.
Finally handle the event SelectionChanged in the code behind the view to change the scroll position so that it always displays the selected item.
For me the benefit of this method is that the viewmodel is kept unaware of anything about the view which is one of the aims of MVVM. The code behind the view is the perfect place for any code concerning the view only.
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ListView x:Name="View"
SelectionChanged="Selector_OnSelectionChanged" IsSynchronizedWithCurrentItem="True" ItemsSource="{Binding Items}"/>
<Button Grid.Row="1" Command="{Binding ChangeSelectionCommand}">Set</Button>
</Grid>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel();
}
private void Selector_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
View.ScrollIntoView(View.SelectedItem);
}
}
public class ViewModel
{
private readonly CollectionViewSource _source = new CollectionViewSource();
public ICollectionView Items
{
get { return _source.View; }
}
public ICommand ChangeSelectionCommand { get; set; }
public ViewModel()
{
SetUp();
ChangeSelectionCommand = new Command(ChangeSelection);
}
private void SetUp()
{
var list = new List<string>();
for (int i = 0; i < 100; i++)
{
list.Add(i.ToString(CultureInfo.InvariantCulture));
}
_source.Source = list;
}
private void ChangeSelection()
{
var random = new Random(DateTime.Now.Millisecond);
var n = random.Next(100);
Items.MoveCurrentToPosition(n);
}
}
public class Command : ICommand
{
private readonly Action _action;
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
_action();
}
public event EventHandler CanExecuteChanged;
public Command(Action action)
{
_action = action;
}
}
let me share my solution with you
Create your own ListView descendant with dependency property TargetListItem
public class ScrollableListView : ListView
{
/// <summary>
/// Set this property to make ListView scroll to it
/// </summary>
public object TargetListItem
{
get { return (object)GetValue(TargetListItemProperty); }
set { SetValue(TargetListItemProperty, value); }
}
public static readonly DependencyProperty TargetListItemProperty = DependencyProperty.Register(
nameof(TargetListItem), typeof(object), typeof(ScrollableListView), new PropertyMetadata(null, TargetListItemPropertyChangedCallback));
static void TargetListItemPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var owner = (ScrollableListView)d;
owner.ScrollToItem(e.NewValue);
}
public void ScrollToItem(object value)
{
if (value != null && Items != null && Items.Contains(value))
{
ScrollIntoView(value);
}
}
}
create property in ViewModel
object currentListItem;
public object СurrentListItem
{
get => сurrentListItem;
set
{
if (сurrentListItem != value)
{
сurrentListItem = value;
OnPropertyChanged(nameof(СurrentListItem));
}
}
}
bind it
<controls:ScrollableListView ... TargetListItem="{Binding CurrentListItem}"/>
Now you can set CurrentListItem in ViewModel when needed. And the corresponding visual element will become visible in the ListView immediately.
Also maybe you just can use attached property on ListView instead of creating ScrollableListView. But i'm not sure.
Yep, there's always times in MVVM when you need to get at the control. There's various ways of doing this, but here's an easy-ish way of doing it without deriving from the control or messing with routed commands or other such toys what you have in WPF.
In summary:
Create an attached property on your view model.
Set the attached property in XAML to pass the list box back to the view model.
Call .ScrollIntoView on demand.
Note, this is a rough and ready example, make sure your DataContext is set before showing the window.
Code/View Model:
public class ViewModel
{
private ListBox _listBox;
private void ReceiveListBox(ListBox listBox)
{
_listBox = listBox;
}
public static readonly DependencyProperty ListBoxHookProperty = DependencyProperty.RegisterAttached(
"ListBoxHook", typeof (ListBox), typeof (ViewModel), new PropertyMetadata(default(ListBox), ListBoxHookPropertyChangedCallback));
private static void ListBoxHookPropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
var listBox = (ListBox) dependencyObject;
var viewModel = (ViewModel) listBox.DataContext;
viewModel.ReceiveListBox(listBox);
}
public static void SetListBoxHook(DependencyObject element, ListBox value)
{
element.SetValue(ListBoxHookProperty, value);
}
public static ListBox GetListBoxHook(DependencyObject element)
{
return (ListBox) element.GetValue(ListBoxHookProperty);
}
}
OK, so that will let us get the ListBox passed back to the view; you can do with it as you wish.
Now, just set the property in XAML:
<ListBox wpfApplication1:ViewModel.ListBoxHook="{Binding RelativeSource={RelativeSource Self}}" />
Good to go!
My window uses MyUserControl and passes to it a collection of items through MyItemsProperty property. MyUserControl converts the items to MyClassBag and adds the items to WrappedItems collection (OnMyItemsChanged->CollectionChanged->AddItem) which is bound to ItemsSource of DataGrid.
The problem is that ItemsSource is not updated (I set a brakepoint on control.WrappedItems.Add(new MyClassBag { ... }); and it does get there).
But, if I put a button inside MyUserControl and add items (WrappedItems.Add(new MyClassBag { ... });) through button's click method the ItemsSource does get updated. What is the problem?
public partial class MyUserControl : UserControl
{
public static readonly DependencyProperty MyItemsProperty =
DependencyProperty.Register("MyItems",
typeof(ObservableCollection<MyClass>),
typeof(MyUserControl),
new PropertyMetadata(
new ObservableCollection<MyClass>(),
new PropertyChangedCallback(OnMyItemsChanged)
)
);
public ObservableCollection<MyClass> MyItems
{
get { return (ObservableCollection<MyItems>)GetValue(MyItemsProperty); }
set { SetValue(MyItemsProperty, value); }
}
private static void OnMyItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
control = d as MyUserControl;
var items = e.NewValue as ObservableCollection<MyClass>;
states.CollectionChanged += new NotifyCollectionChangedEventHandler(CollectionChanged);
AddItem(control, items);
}
private static void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
var items = sender as ObservableCollection<MyClass>;
if (e.Action == NotifyCollectionChangedAction.Add)
{
control.AddNewItem();
}
}
public ObservableCollection<MyClassBag> WrappedItems { get; set; }
public void AddNewItem()
{
WrappedItems.Add(new MyClassBag { ... });
}
private void Button_Click(object sender, RoutedEventArgs e)
{
AddNewItem();
}
}
MyUserControl.XAML
<DataGrid ItemsSource="{Binding WrappedItems ... />
<Button Click="Button_Click" />
Window.cs
public partial class Window1 : Window
{
public ObservableCollection<MyClass> ItemsForMyUserControl { get; set; }
private void Load()
{
ItemsForMyUserControl.Add(new MyClass(...));
ItemsForMyUserControl.Add(new MyClass(...));
}
}
Window.xaml
<uc:MyUserControl MyItems="{Binding ItemsForMyUserControl}" />
I had a similiar problem some time ago.
Please make the MyClass class inherit from Freezable class and everything may work. Please let me know if it helped.