I'm working on a small WPF application in which I have a combobox that is bound to an ObservableCollection in the code behind:
public Molecule CurrentMolecule { get; set; }
public ObservableCollection<string> Formulas { get; set; }
public MainWindow()
{
CurrentMolecule = new Molecule();
Formulas = new ObservableCollection<string>(CurrentMolecule.FormulasList.ToList());
DataContext = this;
InitializeComponent();
}
<ComboBox x:Name="cmbFormula" ItemsSource="{Binding Path=Formulas}" SelectionChanged="cmbFormula_SelectionChanged"/>
This works fine to populate my combo box with the CurrentMolecule.FormulasList however if at some point I set CurrentMolecule to a new instance of Molecule the databinding no longer works. Do I need to implement some kind of OnPropertyChanged event so that no matter what the contents of the combo box will stay current with the CurrentMolecule.FormulasList?
You have to implement INotifyPropertyChanged, only then the changes will be updated in UI.
Here are the modifications that I've done to your code.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.ComponentModel;
using System.Collections.ObjectModel;
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window, INotifyPropertyChanged
{
private Molecule _CurrentMolecule;
public Molecule CurrentMolecule
{
get
{
return _CurrentMolecule;
}
set
{
_CurrentMolecule = value;
OnPropertyChanged("CurrentMolecule");
Formulas = new ObservableCollection<string>(CurrentMolecule.FormulasList.ToList());
}
}
private ObservableCollection<string> _Formulas;
public ObservableCollection<string> Formulas
{
get { return _Formulas; }
set
{
_Formulas = value;
OnPropertyChanged("Formulas");
}
}
public MainWindow()
{
InitializeComponent();
CurrentMolecule = new Molecule();
DataContext = this;
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
Edit:
A better approach is to create a ViewModel and then bind it to the DataContext of the Window.
Define a new class called ViewModel as below. Note you might want to change the namespace
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Collections.ObjectModel;
namespace WpfApplication1
{
public class ViewModel : INotifyPropertyChanged
{
#region Properties
private Molecule _CurrentMolecule;
public Molecule CurrentMolecule
{
get
{
return _CurrentMolecule;
}
set
{
_CurrentMolecule = value;
OnPropertyChanged("CurrentMolecule");
Formulas = new ObservableCollection<string>(CurrentMolecule.FormulasList.ToList());
}
}
private ObservableCollection<string> _Formulas;
public ObservableCollection<string> Formulas
{
get { return _Formulas; }
set
{
_Formulas = value;
OnPropertyChanged("Formulas");
}
}
#endregion
#region Constructor
public ViewModel()
{
CurrentMolecule = new Molecule();
}
#endregion
#region INotifyPropertyChanged implementation
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
}
Modify the MainWindow code behind file as below
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel();
}
You are probably missing WPF controls datacontext fundamentals. If you are using binding like {Binding CurrentMolecule.FormulasList} or parent controls datacontext is bound to "CurrentMolecule" whenever you swap DataContext of that item, the binding will reset. If you would like to keep the datacontext bound to the FormulasList even when the parent datacontext changes. You need to bind that context directly to FormulasList property and make sure that other parent controls are not using CurrentMolecule as a datacontext, even when its instance changes in your ViewModel.
Related
I am working on a to-do list application for a project. I would like to change the value of a string in an observableCollection. I am able to change the string in the same window but I would like to change the value from a textbox in a secondary window.
So what I tried to do was is change a string in the first window by using a textbox in the second window. By doing the way I have listed below it just blanks out the item I am trying to edit.
I would like to take the test from the textbox in the second window and use it to modify the taskName in the first window. Below I am going to include my code for the two c# files for the windows.
This is the main window but it is called DemoMainWindow:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using ToDoList.ViewModels;
using ToDoList.Model;
namespace ToDoList
{
/// <summary>
/// Interaction logic for DemoMainWindow.xaml
/// </summary>
public partial class DemoMainWindow : Window
{
private ViewModel _viewModel;
public string? EditedTaskName { get; set; }
public DemoMainWindow()
{
InitializeComponent();
TxtUCEnteredTask.txtLimitedInput.Text = "Do the dishes";
_viewModel = new ViewModel();
DataContext = _viewModel;
}
private void BtnAddTask_Click(object sender, RoutedEventArgs e)
{
_viewModel.Tasks.Add(new TaskModel() { TaskName = TxtUCEnteredTask.txtLimitedInput.Text });
}
private void BtnDeleteTask_Click(object sender, RoutedEventArgs e)
{
if(LstBoxTasks.SelectedItem != null)
{
#pragma warning disable CS8604 // Possible null reference argument.
_ = _viewModel.Tasks.Remove(item: LstBoxTasks.SelectedItem as TaskModel);
#pragma warning restore CS8604 // Possible null reference argument.
}
}
private void BtnHelp_Click(object sender, RoutedEventArgs e)
{
HelpWindow helpWindow = new HelpWindow();
helpWindow.Show();
}
private string? GetEditedTaskName()
{
return EditedTaskName;
}
private void BtnEditTask_Click(object sender, RoutedEventArgs e)
{
if (LstBoxTasks.SelectedItem != null)
{
EditWindow editWindow = new EditWindow();
editWindow.Show();
//_viewModel.Tasks[LstBoxTasks.SelectedIndex].TaskName = editedTaskName;
}
}
}
}
This is the code for the C# file of the second window:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace ToDoList
{
/// <summary>
/// Interaction logic for EditWindow.xaml
/// </summary>
public partial class EditWindow : Window
{
public EditWindow()
{
InitializeComponent();
var DemoMainWindow = this.DataContext;
}
private void BtnEdit_Click(object sender, RoutedEventArgs e)
{
((DemoMainWindow)Application.Current.MainWindow).EditedTaskName = EditTextBox.Text;
// _viewModel.Tasks[LstBoxTasks.SelectedIndex].TaskName = TxtUCEnteredTask.txtLimitedInput.Text;
}
}
}
When you create the view, create it's viewmodel before so that you can set the view's datacontext:
EditWindowViewModel vm = new EditWindowViewModel();
EditWindow editWindow = new EditWindow()
{
DataContext = vm;
};
editWindow.Show();
StringToChange = vm.EditBoxTextProperty;
Create a bindable property for stroing the editbox's text using INotifyPropertyCganged in the EditWindowViewModel (see this):
private string _editBoxTextProperty;
public string EditBoxTextProperty
{
get => _editBoxTextProperty;
set
{
if (_editBoxTextProperty != value)
{
_editBoxTextProperty = value;
OnPropertyChanged();
}
}
}
In the EditWindow xaml, use binding to connect the editbox's text value to your EditBoxTextProperty.
<TextBox Text="{Binding Path=EditBoxTextProperty}"/>
Usefull links to build a proper WPF application: DataBinding MVVM Pattern
What I have found to be useful when having to interact between elements on different windows is using x:FieldModifier
<TextBox x:Name="textbox" x:FieldModifier="public" />
You can then access the element through the instance of you window
it will appear as WindowInstanceName.textbox
in my application I'm opening a Window for an Input form. In my App.xaml I have defined the following:
<DataTemplate DataType="{x:Type ViewModels:EditTicketViewModel}">
<Frame>
<Frame.Content>
<Views:EditTicketView></Views:EditTicketView>
</Frame.Content>
</Frame>
</DataTemplate>
My application also has a Window service for opening windows:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DevPortal.Interfaces
{
public interface IWindowService
{
public void ShowWindow(object viewModel, bool showDialog);
}
}
the implementation:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using DevPortal.Interfaces;
using Syncfusion.Windows.Shared;
using Syncfusion.SfSkinManager;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace DevPortal.Services
{
public class WindowService : IWindowService
{
public void ShowWindow(object viewModel, bool showDialog)
{
var window = new ChromelessWindow();
window.ResizeMode = ResizeMode.NoResize;
SfSkinManager.SetTheme(window, new Theme("FluentDark"));
window.Content = viewModel;
window.SizeToContent = SizeToContent.WidthAndHeight;
window.Title = viewModel.GetType().GetProperty("Title").GetValue(viewModel).ToString();
window.ShowIcon = false;
if (showDialog)
{
window.ShowDialog();
} else
{
window.Show();
}
}
}
}
How I open the window (from a viewmodel in the MainView)
[RelayCommand]
private void CreateTicket()
{
App.Current.ServiceProvider.GetService<IWindowService>().ShowWindow(new EditTicketViewModel(), true);
}
What would be the best way to close this window from the ViewModel? Previously i was used to directly create the view, and in the constructor of the view i would subscribe to a close-event in the viewmodel, but that's not really the MVVM-way I guess. Do I need to implement some kind of service? Thanks!
EDIT: I forgott to mention that the View is a page. So i Am creating a window with the viewmodel as content, and the datatemplate of the viewmodel is a Frame containing the page.
You could for example return an IWindow from your window service:
public class WindowService : IWindowService
{
public IWindow ShowWindow(object viewModel, bool showDialog)
{
var window = new ChromelessWindow();
...
return window;
}
}
...and then simply call Close() on this one in the view model.
The interface would be as simple as this:
public interface IWindow
{
void Close();
}
Your ChromelessWindow implements the interface:
public partial class ChromelessWindow : Window, IWindow { ... }
...and the view model only has a dependency on an interface. It still doesn't know anything about a view or actual window. IWindow is just a name. I can be called anything.
The cleanest way of closing a Window from it's ViewModel is using an attached property.
public static class perWindowHelper
{
public static readonly DependencyProperty CloseWindowProperty = DependencyProperty.RegisterAttached(
"CloseWindow",
typeof(bool?),
typeof(perWindowHelper),
new PropertyMetadata(null, OnCloseWindowChanged));
private static void OnCloseWindowChanged(DependencyObject target, DependencyPropertyChangedEventArgs args)
{
if (!(target is Window view))
{
return;
}
if (view.IsModal())
{
view.DialogResult = args.NewValue as bool?;
}
else
{
view.Close();
}
}
public static void SetCloseWindow(Window target, bool? value)
{
target.SetValue(CloseWindowProperty, value);
}
public static bool IsModal(this Window window)
{
var fieldInfo = typeof(Window).GetField("_showingAsDialog", BindingFlags.Instance | BindingFlags.NonPublic);
return fieldInfo != null && (bool)fieldInfo.GetValue(window);
}
}
In the ViewModel create a ViewClosed property
private bool? _viewClosed;
public bool? ViewClosed
{
get => _viewClosed;
set => Set(nameof(ViewClosed), ref _viewClosed, value);
}
then in the View, bind to it using the attached property
<Window
...
vhelp:perWindowHelper.CloseWindow="{Binding ViewClosed}" >
Mode details on my take on MVVM navigation on my blog post.
I've seen lots of similar questions but I've not been able to find either a question/answer or tutorial which clearly lists out all of the components required to get this to work. I'm trying to follow MVVM but as this is entirely a UI concern I'm not against doing some code-behind.
What I am trying to achieve:
ListView.ItemsSource bound to an ObservableCollection<T>
Filter the displayed items in ListView based on a TextBox
Filter is updated as user types in TextBox
In my ViewModel I have something like this:
private ObservableCollection<Customer> _customers;
public ObservableCollection<Customer> Customers
{
get { return _customers; }
set
{
_customers= value;
RaisePropertyChanged("Customers");
}
}
private Customer _selected_Customer;
public Customer Selected_Customer
{
get { return _selected_Customer; }
set
{
_selected_Customer= value;
RaisePropertyChanged("Selected_Customer");
}
}
private string _filtered_Name;
public string Filtered_Name
{
get { return _filtered_Name; }
set
{
_filtered_Name = value;
RaisePropertyChanged("Filtered_Name");
}
}
And in my XAML it's like this:
<CollectionViewSource x:Key="cvs"
x:Name="Customer_Details_View"
Source="{Binding Path=Customers}"/>
<TextBox x:Name="Filtered_Name" Text="{Binding Filtered_Name, Mode=TwoWay}"/>
<ListView ItemsSource="{Binding ElementName=Customer_Details_View}"
SelectedItem="{Binding Selected_Customer, Mode=TwoWay}">
I want to filter my ObservableCollection<Customer> with the following logic: Customer.Name.ToLower().Contains(Filtered_Name.ToLower())
How to I bind the TextBox.Text to the CollectionViewSource or utilize the CollectionViewSource.Filter event to apply the above filter?
You'll probably want to declare the collectionviewsource in your VM and bind directly to it. Once you've done that you can change the filter as answered in this question CollectionViewSource, how to filter data?
VM Example, may not work without massaging.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Data;
namespace CollectionViewSourceExample
{
class MainWindowViewModel : INotifyPropertyChanged
{
private string _filter;
public MainWindowViewModel()
{
SourceCollection = new ObservableCollection<string>();
SourceCollection.Add("Fred Flintstone");
SourceCollection.Add("Wilma Flintstone");
SourceCollection.Add("Bambam Flintstone");
SourceCollection.Add("Barny Rubble");
SourceCollection.Add("Betty Rubble");
ViewSource = (ListCollectionView)CollectionViewSource.GetDefaultView(SourceCollection);
ViewSource.Filter = SourceFilter;
ViewSource.IsLiveFiltering = true;
}
private bool SourceFilter(object obj)
{
string val = (string)obj;
return string.IsNullOrWhiteSpace(Filter) || val.Contains(Filter);
}
public string Filter
{
get { return _filter; }
set
{
_filter = value;
PropertyChanged(this, new PropertyChangedEventArgs("Filter"));
ViewSource.Refresh();
}
}
public ObservableCollection<string> SourceCollection { get; }
public ListCollectionView ViewSource { get; }
public event PropertyChangedEventHandler PropertyChanged;
}
}
I want to execute a method on TextChange event and for specific text, I want to do something and close the window using MVVM
for example on this part of my code I want to close my window:
if (text.Equals("12345"))
{
//Exit from window
}
I know how can I do it from a button using a command, As I have in the code of the next example.
But how can I pass the window to a running property that binds to a text of the textbox?
or there is another way to close form?
I want to do it on the ViewModel and not the code behind to write my code as MVVM pattern close that I can
my XAML code :
<Window x:Class="PulserTesterMultipleHeads.UserControls.TestWindows"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:PulserTesterMultipleHeads.UserControls"
mc:Ignorable="d"
Name="MainTestWindow"
Title="TestWindows" Height="450" Width="800">
<Grid>
<StackPanel>
<TextBox Text="{Binding Text,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"></TextBox>
<Button Command="{Binding EndTestExit}"
CommandParameter="{Binding ElementName=MainTestWindow}">Exit</Button>
</StackPanel>
</Grid>
</Window>
the code behind XAML :
using PulserTesterMultipleHeads.Classes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace PulserTesterMultipleHeads.UserControls
{
/// <summary>
/// Interaction logic for TestWindows.xaml
/// </summary>
public partial class TestWindows : Window
{
public TestWindows()
{
InitializeComponent();
DataContext = new TestWindowsMV();
}
}
}
View Model code :
using PulserTester.ViewModel.Base;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
namespace PulserTesterMultipleHeads.Classes
{
public class TestWindowsMV : INotifyPropertyChanged
{
private string text;
public string Text {
get {
return text;
} set {
text = value;
if (text.Equals("12345"))
{
//Exit from window
}
}
}
/// <summary>
/// for the example
/// </summary>
private ICommand _EndTestExit;
public ICommand EndTestExit
{
get
{
if (_EndTestExit == null)
{
_EndTestExit = new GenericRelayCommand<Window>((window) => EndTestExitAction(window));
}
return _EndTestExit;
}
}
private void EndTestExitAction(Window window)
{
window.Close();
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
************************* Edit as the answer ****************************
The change in ModelView :
public string Text {
get {
return text;
} set {
text = value;
if (text.Equals("12345"))
{
CloseAction();
}
}
}
The change in code behind:
public TestWindows()
{
InitializeComponent();
DataContext = new TestWindowsMV();
if (((TestWindowsMV)DataContext).CloseAction == null)
((TestWindowsMV)DataContext).CloseAction = new Action(this.Close);
}
It is kind of hard with MVVM but what you can do is create an Event inside of your ViewModel and attach a function that will close your window in code behind of your View. Then you will be able to call your event inside of ViewModel whenever you need to close the window (or do something else that is more convenient to do in View rather than in ViewModel)
The cleanest way to close a View from the ViewModel is to use an attached property
public static class perWindowHelper
{
public static readonly DependencyProperty CloseWindowProperty = DependencyProperty.RegisterAttached(
"CloseWindow",
typeof(bool?),
typeof(perWindowHelper),
new PropertyMetadata(null, OnCloseWindowChanged));
private static void OnCloseWindowChanged(DependencyObject target, DependencyPropertyChangedEventArgs args)
{
if (!(target is Window view))
return;
if (view.IsModal())
view.DialogResult = args.NewValue as bool?;
else
view.Close();
}
public static void SetCloseWindow(Window target, bool? value)
{
target.SetValue(CloseWindowProperty, value);
}
public static bool IsModal(this Window window)
{
var fieldInfo = typeof(Window).GetField("_showingAsDialog", BindingFlags.Instance | BindingFlags.NonPublic);
return fieldInfo != null && (bool)fieldInfo.GetValue(window);
}
}
which you can then bind to an appropriate property in your ViewModel
<Window
x:Class="...
vhelp:perWindowHelper.CloseWindow="{Binding ViewClosed}">
private bool? _viewClosed;
public bool? ViewClosed
{
get { return _viewClosed; }
set { Set(nameof(ViewClosed), ref _viewClosed, value); }
}
More details on my recent blog post.
You have almost everything you need already implemented. All you really need to do is call private void EndTestExitAction(Window window) from your setter and supply window its value during construction:
public string Text {
get {
return text;
} set {
text = value;
if (text.Equals("12345"))
{
EndTestExitAction(window)
}
}
}
where window is a property of your View Model.
here is the problem i am encountering, i will use the class names in my demo to describe my problem:
i have to show something on a datagrid, as a view of Item. but the Item itself is a wrapper class of the real data source, Dummy.
Container is the manager of all Dummy's, it will call Poll periodically(in my demo, 1s) and it internally queries the remote server to decide the current value(in demo, i simply flip the boolean). and after that, Container will go through all Item's and Notify them changes may be made.
but my problem is, the if branch in Notify called by Container won't get in, since PropertyChanged is null! but if i change it in the DataGrid, i.e. dg, by double click, this event gets subscribed. so my problem is, how can i successfully notify in Container? is there any a way to make DataGrid subscribe to PropertyChanged all the time?
btw, if i use a debugger to get in, they are null too.
Following is my demo code:
xaml:
<Window x:Class="WpfApplication6.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<DataGrid x:Name="dg" Margin="0,0,0,35"/>
<Button Content="Button" Margin="0,0,10,10" Height="20" VerticalAlignment="Bottom" HorizontalAlignment="Right" Width="75" Click="Button_Click"/>
</Grid>
</Window>
c# part:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;
namespace WpfApplication6
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private Container _container;
public MainWindow()
{
InitializeComponent();
_container = new Container();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
dg.ItemsSource = _container.GetItems();
}
}
public class Dummy
{
public Dummy()
{
_done=false;
}
private bool _done;
public bool Done {
get { return _done; }
// set will trigger something remotely too.
set { _done = value; }
}
public void Poll() { _done=!_done;}
}
public class Item : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private Dummy dummy;
public Item(Dummy dum)
{
dummy = dum;
}
public bool Done
{
get { return dummy.Done; }
set
{
dummy.Done = value;
Notify();
}
}
public void Notify()
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs("Done"));
}
}
public class Container
{
private List<Dummy> dummies;
private DispatcherTimer _updateTimer;
public Container()
{
dummies = (from i in Enumerable.Range(0, 10)
select new Dummy()).ToList();
}
private IEnumerable<Item> items;
public Item[] GetItems()
{
if (_updateTimer != null)
_updateTimer.Stop();
items=dummies.Select(x => new Item(x));
_updateTimer = new DispatcherTimer();
_updateTimer.Interval = TimeSpan.FromSeconds(1);
_updateTimer.Tick += (_1, _2) =>
{
foreach (var item in dummies)
{
item.Poll();
}
foreach (var item in items)
{
item.Notify();
}
};
_updateTimer.Start();
return items.ToArray();
}
}
}