Hybrid MVVM implementation for a PropertyGrid (in DevExpress) - c#

I need your help! Following is basically what I have in my main XAML view :
<Button x:Name="button1" Content= "{Binding Customer1, Mode=TwoWay}" Margin="271,52,103,106" Click="button1_Click" />
The code-behind of the main XAML (Code-behind, since it's not a 100% pure MVVM, and a rather hybrid one) goes like this :
public MainWindow()
{
InitializeComponent();
this.DataContext = new MyViewModel();
}
private void button1_Click(object sender, RoutedEventArgs e)
{
DXDialog d = new DXDialog("Information", DialogButtons.OkCancel,true);
d.Content = new PropertyGrid();
d.SizeToContent = System.Windows.SizeToContent.WidthAndHeight;
d.Owner = this;
d.WindowStartupLocation = System.Windows.WindowStartupLocation.CenterOwner;
var result = d.ShowDialog();
if (result == true)
{
}
}
As you can see, I have a Button whose content is bound to a String property in the ViewModel Class. Upon Clicking the button, I'm opening a DXDialog which contains a PropertyGrid with the Properties of the ViewModel class. Let me show you my ViewModel Class below :
public class MyViewModel : ViewModelBase, INotifyPropertyChanged
{
Customer currentCustomer;
protected string _customer1;
public string Customer1 {
get { return this._customer1; }
set { this.SetProperty(ref this._customer1, value, "Customer1"); }
}
public MyViewModel()
{
//Customers = new ObservableCollection<Customer>();
//Customers.Add(new Customer() { Name = "Name1" });
Customer1 = "ABC";
}
}
In the Dialog I'm being able to edit the value of the property but don't yet know how I can save it in a way that it immediately reflects even on the button of the main View {Reflects everywhere it must be bound to, I mean}. I can see the execution coming to the following line in the main code behind
if (result == true)
{
}
But I don't know how to get the edited values and plug them into the right place.
Basically, My requirement is to have multiple controls (Buttons, in this case) bound to multiple instances of a ViewModel class, and then, upon clicking the buttons, I should be able to edit those specific ViewModel instances inside the PropertyGrid of the DXDialogue, and after clicking "Ok", the changes should reflect on the relevant buttons as well.
-Ron

To display ViewModel's properties in the PropertyGrid, assign the ViewModel to its SelectedObject property,and make sure that the ShowProperties option is set to All.
Changes will be reflected in buttons bound to the ViewModel only of you use one and the same ViewModel instance in the main and the dialog windows.
var grid = new PropertyGrid();
grid.SelectedObject = this.DataContext;
grid.ShowProperties = ShowPropertiesMode.All;
d.Content = grid;

Related

Upload dataGrid in Wpf MVVM project

Here is the class of ChildViewModel:
public class ChildViewModel : Screen
{
private string imie = string.Empty;
private string nazwisko = string.Empty;
private string wiek = string.Empty;
private Person person;
private ObservableCollection<Person> personColl;
private MainViewModel mainView = new MainViewModel();
public ChildViewModel(Person person, ObservableCollection<Person> personColl)
{
this.person = person;
this.personColl = personColl;
this.Wyswietl();
}
public string ImieTxt
{
get => this.imie;
set
{
this.imie = value;
this.NotifyOfPropertyChange(() => this.ImieTxt);
}
}
public string NazwiskoTxt
{
get => this.nazwisko;
set
{
this.nazwisko = value;
this.NotifyOfPropertyChange(() => this.NazwiskoTxt);
}
}
public string WiekTxt
{
get => this.wiek;
set
{
this.wiek = value;
this.NotifyOfPropertyChange(() => this.WiekTxt);
}
}
public void Zmien()
{
this.personColl[mainView.DataGridIndex].Imie = this.ImieTxt;
this.personColl[mainView.DataGridIndex].Nazwisko = this.NazwiskoTxt;
this.personColl[mainView.DataGridIndex].Wiek = this.WiekTxt;
this.TryClose();
}
private void Wyswietl()
{
this.ImieTxt = this.person.Imie;
this.NazwiskoTxt = this.person.Nazwisko;
this.WiekTxt = this.person.Wiek;
}
}
I have no idea how to upload new data from ChildView to dataGrid in MainView, after clicking button "Zmien". In MainView I have dataGrid, where from MainViewModel I'm loading data from the list. After clicking button "Zmien", new data doesn't load in dataGrid.
Maybe you have any idea how to do it?
From my article on Codeproject Guide to WPF DataGrid Formatting Using Bindings:
Connecting a DataGrid with Business Data
Even connecting a DataGrid with the business data is not trivial. Basically, a CollectionViewSource is used to connect the DataGrid with the business data:
The CollectionViewSource does the actual data navigation, sorting, filtering, etc.
<Window.Resources>
<CollectionViewSource x:Key="ItemCollectionViewSource" CollectionViewType="ListCollectionView"/>
</Window.Resources>
<DataGrid
DataContext="{StaticResource ItemCollectionViewSource}"
ItemsSource="{Binding}"
AutoGenerateColumns="False"
CanUserAddRows="False">
//create business data
var itemList = new List<stockitem>();
itemList.Add(new StockItem {Name= "Many items", Quantity=100, IsObsolete=false});
itemList.Add(new StockItem {Name= "Enough items", Quantity=10, IsObsolete=false});
...
//link business data to CollectionViewSource
CollectionViewSource itemCollectionViewSource;
itemCollectionViewSource = (CollectionViewSource)(FindResource("ItemCollectionViewSource"));
itemCollectionViewSource.Source = itemList;
Define a CollectionViewSource in Windows.Resource
The gotcha here is that you must set the CollectionViewType. If you
don't, the GridView will use BindingListCollectionView, which does
not support sorting. Of course, MSDN does not explain this anywhere.
Set the DataContext of the DataGrid to the CollectionViewSource.
In the code behind, find the CollectionViewSource and assign your business data to the Source property
In this article, data gets only read. If the user should be able to edit the data, use an ObservableCollection. However, it is often better to leave the DataGrid readonly, because editing in the DataGrid behaves differently from what one is used from spreadsheet programs. It might be better if the user has to doubleclick on the row he wants to change and open another window just for editing that entity or adding a new one.

Two questions about mvvm navigation of pages

I am trying to make a template-translator (.doc) from EN to other languages.
It is just for me.
I have already done simple mvvm navigation. For clear understanding what do I want, check picture:
First question: How do I translate ICommand from button "NextItem" to current selected page that has changed a item inside textBox, otherwise how do I Call Translate() method from current page for my Button which is in the MainView?
Second question: How do I put all pages that I have on the Window in the Combobox on the Upper side window, and select page from there, like I do this with my Buttons.
How it is now:
<Button
x:Name="ButtonSecondView"
Width="200"
Command="{Binding GoToSecondViewCommand}"
Content="SecondView" />
<Button
x:Name="ButtonNextItem"
Grid.Row="2"
Width="250"
Command="{Binding NextRandomItem}"
Content="Next item" />
MyCollection is just a stub which generates random items(1 item, 3 item, etc...).
There I can translate some parameters to page while it is initializing.
public MainViewModel()
{
MyCollection = new MyCollection();
CurrentViewModel = new FirstViewModel(this,MyCollection.GetRandomItem());
PageList = MyCollection.GetList();
}
public ICommand GoToFirstViewCommand
{
get
{
return new RelayCommand(() => { CurrentViewModel = new FirstViewModel(this, MyCollection.GetRandomItem()); });
}
}
public ICommand GoToSecondViewCommand
{
get
{
return new RelayCommand(() => { CurrentViewModel = new SecondViewModel(this, MyCollection.GetRandomItem()); });
}
}
ctor in SecondViewModel
public SecondViewModel(INotifyContentChanged contentChanged,string Parametrs)
{
ContentChanged = contentChanged;
TextContent = Parametrs;
}
One more time: First question.
I have many pages (in there 3), and I need to click the button on bottom, and in my page. In my current page I get text from textBox, and input these parameters to my method, like Translate(string field1). And this works on all pages that I want. If I change page in which I select Combobox items, I can do the same button click to button, and text from textBox inputted in my method Translate(string field1).
To navigate and pass the parameters to the corresponding page view models I stick to your pattern and used composition. I introduced a composition container that holds all page view models in a Dictionary<string, IPageViewModel>. Therefore all page view models must implement this interface. As the key I used the page view model's type name (e.g. nameof(FirstViewModel)). I also introduced a new property called PageNavigationParameter that binds to the TextBox in order to get the content (which is supposed to be passed to the corresponding page view model).
A second Dictionary<string, string> maps the display name of each page view model (the page name to be displayed in the ComboBox) to the actual page view model name (that matches the class name e.g. nameof(FistViewModel)). This way you can get the desired page view model by class name or if in the navigation scope from the page display name.
To select pages from a ComboBox you could do this:
create a collection of page names in the view model and bind it to the ComboBox.ItemSource
bind the ComboBox.SelectedItem property to the view model
navigate to page when the view model's property changed
To make this example work you need a common interface that all page view models must implement (e.g. class FirstViewModel : IPageViewModel). This interface must contain at least the PageNavigationParameter
The page view model interface
interface IPageViewModel
{
string PageNavigationParameter { get; set; }
}
Main view model (using composition)
class MainViewModel
{
public MainViewModel()
{
// The Dictionary to get the page view model name
// that maps to a page display name
this.PageViewModelNameMap = new Dictionary<string, string>()
{
{"First Page", nameof(FirstViewModel)},
{"Second Page", nameof(SecondViewModel)}
};
// The ComboBox's items source
// that holds the page view model display names
this.PageNames = new ObservableCollection<string>(this.PageViewModelNameMap.Keys);
// The Dictionary that stores all page view models
// that can be retrieved by the page view model type name
this.PageViewModels = new Dictionary<string, IPageViewModel>()
{
{nameof(FirstViewModel), new FirstViewModel()},
{nameof(SecondViewModel), new SecondViewModel()}
};
this.CurrentPageViewModel = this.PageViewModels[nameof(FirstViewModel)];
this.PageNavigationParameter = string.Empty;
}
// You can use this method as execute handler
// for your NavigateToPage command too
private void NavigateToPage(object parameter)
{
if (!(parameter is string pageName))
{
return;
}
if (this.PageViewModelNameMap.TryGetValue(pageName, out string pageViewModelName)
{
if (this.PageViewModels.TryGetValue(pageViewModelName, out IPageViewModel pageViewModel)
{
pageViewModel.PageNavigationParameter = this.PageNavigationParameter;
this CurrentPageViewModel = pageViewModel;
}
}
}
private bool CanExecuteNavigation(object parameter) => parameter is string destinationPageName && this.PageViewModelNameMap.Contains(destinationPageName);
private void OnSelectedPageChanged(string selectedPageName)
{
NavigateToPage(selectedPageName);
}
private ObservableCollection<string> pageNames;
public ObservableCollection<string> PageNames
{
get => this.pageNames;
set
{
this.pageNames = value;
OnPropertyChanged();
}
}
private string selectedPageName;
public string SelectedPageName
{
get => this.selectedPageName;
set
{
this.selectedPageName = value;
OnPropertyChanged();
OnSelectedPageChanged(value);
}
}
private string pageNavigationParameter;
public string PageNavigationParameter
{
get => this.pageNavigationParameter;
set
{
this.pageNavigationParameter= value;
OnPropertyChanged();
}
}
private Dictionary<string, ViewModelBase> pageViewModels;
public Dictionary<string, ViewModelBase> PageViewModels
{
get => this.pageViewModels;
set
{
this.pageViewModels = value;
OnPropertyChanged();
}
}
private Dictionary<string, string> pageViewModelNameMap;
public Dictionary<string, string> PageViewModelNameMap
{
get => this.pageViewModelNameMap;
set
{
this.pageViewModelNameMap = value;
OnPropertyChanged();
}
}
private IPageViewModel currentPageViewModel;
public IPageViewModel CurrentPageViewModel
{
get => this.currentPageViewModel;
set
{
this.currentPageViewModel= value;
OnPropertyChanged();
}
}
}
The controls that have a cross page scope must have the MainViewModel as their DataContext.
XAML snippet
<!-- The page menu (DataContext is MainViewModel) -->
<ComboBox SelectedItem="{Binding SelectedPageName}" ItemsSource="{Binding PageNames}" />
<!-- The navigation parameter TextBox (DataContext is MainViewModel) -->
<TextBox Text="{Binding PageNavigationParameter}" />
For your navigation button commands you can use the same MainViewModel.NavigateToPage() method as the execute delegate handler and CanExecuteNavigation as the can execute handler. So you now have a single navigation command (e.g. NavigateToPage) that navigates to the destination page by passing the page display name as CommandParameter.

Databinding with unnamed textboxes and listbox

I have been taught lately when using WPF and databinding it is good practice to not name any of the fields but only to associate them with the properties in the other classes. My problem right now is how do I add the data from 3 textboxes (the user enters), save the binded information to the model which then posts the account information into the listbox on the side. I need to add the data to my model. My code from main.xaml is below:
<ListBox ItemsSource="{Binding Path=Files}" SelectedItem="{BindingPath=CurrentItem}" />
<TextBox Text="{Binding Path=bankaccount}"/>
<TextBox Text="{Binding Path=accountnumber}"/>
<TextBox Text="{Binding Path=accounttype}"/>
<Button Content="Save Data To Listbox" Click="Save_Click"/>
Now I will show my FileModel class which holds all of my properties which will be from the textboxes
private short _BankAccount;
private long _AccountNumber;
private char _AccountType;
public short bankaccount{ get { return _BankAccount;} set {_BankAccount= value; Notify("bankaccount"); } }
public long accountnumber{ get { return _AccountNumber;} set {_AccountNumber= value; Notify("accountnumber"); } }
public char accounttype{ get { return _AccountType;} set{_AccountType= value; Notify("accounttype"); } }
I use a class called ProgramModel As my middle point between the Mainpage and my FileModel page and here is that code:
public class ProgramModel : INotifyPropertyChanged
{
public ObservableCollection<FileModel> Files { get; set; }
private FileModel _currentItem;
public FileModel CurrentItem { get { return _currentItem; } set { _currentItem = value; Notify("CurrentItem"); } }
public ProgramModel()
{
Files = new ObservableCollection<FileModel>();
}
And to finish it off I have my mainpage:
internal partial class MainWindow
{
public ProgramModel Model { get; set; }
private ViewSettings _viewSettings = new ViewSettings();
public MainWindow()
{
InitializeComponent();
this.DataContext = Model = new ProgramModel();
}
private void Save_Click(object sender, RoutedEventArgs e)
{
FileModel filemodel = new FileModel();
Model.Files.Add(new FileModel( filemodel.bankaccount, filemodel.accountnumber, filemodel.accounttype));
}
I feel like I am adding to the Files Collection incorrectly from the save button event. If you guys can help me out that would be great! All 3 textboxes and the listbox are on the Main page. Let me know if you have any questions. Also, this is a learning experience so let me know if I posted too much code or not enough. Thanks!
You read the values from a new FileModel instance instead of from what is bound to the view. Code should be this:
Model.Files.Add(new FileModel
(
Model.CurrentItem.bankaccount,
Model.CurrentItem.accountnumber,
Model.CurrentItem.accounttype
));
Make sure CurrentItem is actually initialized with an instance, don't see that in your code. Also, you could use a command here and have all the relevant logic in your bound view model without the need for the event.
Also, right now you bind the current item to the selected item in the ListBox, this will modify an existing instance instead. Not sure if this is intended. If you want those fields to be for input of new instances don't bind the ListBox to it.
I'm not going to answer your question directly because implementing proper data binding will take a bit of code to do so.
Using proper data binding, it is possible to have almost no code behind on your view.cs! (Specially if you start using frameworks)
Please take a look on A Simple MVVM Example for you to follow good practice.
By following this example, you will see that you can also use data binding on buttons and other controls.
Your View Model which is ProgramModel : INotifyPropertyChanged should handle all the work (data processing).
Your model should not handle the UI update notifications thus,
public short bankaccount{ get { return _BankAccount;} set {_BankAccount= value; Notify("bankaccount"); } }
will be moved to the ProgramModel (View Model).
Save_Click method will also be converted into an ICommand and be binded to the button in view like <Button Content="Save Data To Listbox" Command="{Binding SaveExec}"/>
The point is, if you are studying data binding, you should implement it right. Hope you understand...
In the end, it is possible for your Main.cs to only be..
internal partial class MainWindow
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new ProgramModel();
}
}
Just a small change and it should work . Change Your bindings as shown below for the TextBoxes.
<TextBox Text="{Binding Path=CurrentItem.bankaccount}"/>
<TextBox Text="{Binding Path=CurrentItem.accountnumber}"/>
<TextBox Text="{Binding Path=CurrentItem.accounttype}"/>

Using MVVM instead of main window for the following code

I'm using the following code which is copy pasted from the main window which was working as expected ,
I have created View which is user control and put the code of the
code from the main window XAML
In the View model I put reference for the User model
In the user control I put the code for from the the main window which
is related to the event handlers for example the
DropText_PreviewDragEnter & listbox_SelectionChanged
Currently I have 2 issues in the User Control which Im not sure how to overcome...
1. Errors in the user control for all the occurrence of the ListBox (for example from listbox_SelectionChanged ystem.Windows.Controls.ListBox.SelectedItems.Count > 0 . the Selected items are marked at red with the following error
"cannot access non-static property SelectedItems item source in static context". ,not sure what is the reason since in the main window it was the same as static.
2. Since I have copied the code from the main window there is references to user object in the user controlwhich I believe is not acceptable in MVVM ,how should I change it ? for example
var mySelectedItem = System.Windows.Controls.ListBox.SelectedItem as User;
or
bool remove = _UsersList.Remove((User) System.Windows.Controls.ListBox.SelectedItem);
Here is the code.
I will appreciate your help !
The view model
public partial class ModelView : UserControl
{
private const string DRAG_SOURCE = "DragSource";
public ModelView()
{
InitializeComponent();
DataContext = new ModelView();
}
//Drag Over from text box to List box
private void ListBox_PreviewDrop(object sender, DragEventArgs e)
{
object dragSource = e.Data.GetData(DRAG_SOURCE);
if (dragSource != null && dragSource is TextBox)
{
(dragSource as TextBox).Text = String.Empty;
}
if (!String.IsNullOrEmpty(e.Data.GetData(DataFormats.StringFormat).ToString()) && dragSource is TextBox)
{
_UsersList.Add(new User {Name = e.Data.GetData(DataFormats.StringFormat).ToString()});
}
else
{
e.Handled = true;
}
}
}
}
The Xaml is
<TextBox x:Name="name1"
AcceptsReturn="True"
AllowDrop="True"
PreviewDragEnter="DropText_PreviewDragEnter"
PreviewDrop="DropText_PreviewDrop"
PreviewMouseDown="DropText_PreviewMouseDown"
HorizontalAlignment="Left" Height="20" Margin="360,70,0,0" TextWrapping="Wrap" Text=""
VerticalAlignment="Top" Width="70"/>
....
The model view
internal class ModelView
{
private ObservableCollection<User> _UsersList = new ObservableCollection<User>();
public ObservableCollection<User> UserList
{
get { return _UsersList; }
}
public void InitUsers()
{
_UsersList.Add(new User {Name = "fff"});
//Sort the User collection
ICollectionView usersView = CollectionViewSource.GetDefaultView(_UsersList);
usersView.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
}
}
You already have two answers explaining why the first issue happend in the previous question. And follwoing points are what #Will said in comment as a mess in MVVM implementation that I can see in your codes :
UsersList in the model view is a Model as in Model-View-ViewModel.
And the model view it self is a ViewModel as in Model-View-ViewModel
Then what you call view model is actually a View in Model-View-ViewModel point of view. It inherits UserControl and UserControl is a view, no difference from Window or Page, etc. They're all View. And even if we agree to call it view model, then it violated MVVM principle everywhere, because view model shouldn't have reference to View/UI control object.
Not directly answering your question, but I hope you get a better prespective on MVVM pattern.
#phil correctly noted that you can't access the ListBox like this:
System.Windows.Controls.ListBox
What he failed to mention is that you shouldn't access a ListBox at all if you're using MVVM. Clearly you're not using MVVM now, but if you want to, then I would recommend that you read up on it so that you can get the full benefit from it. Just having a view and a view model does not mean that you're using MVVM.
In MVVM, we manipulate data, not UI controls. Therefore, you need to create a SelectedItem property in your view model and bind that to the ListBox.SelectedItem property and then you'll always have access to the item that is selected:
public User SelectedItem { get; set; } // Implement INotifyPropertyChanged here
...
<ListBox ItemsSource="{Binding YourCollection}" SelectedItem="{Binding SelectedItem}"/>
Now you can do something with the selected item like this:
string selectedItemName = SelectedItem.Name;
you have to access your listbox by
yourListBoxName.SelectedItems.Count > 0
you can't access it by
System.Windows.Controls.ListBox.SelectedItems.Count
same for
var mySelectedItem = System.Windows.Controls.ListBox.SelectedItem as User;
use the following instead
var mySelectedItem = yourListBoxName.SelectedItem as User;

Preselection in WPF-DataGrid with MVVM

I've got DataGrid bound to an ObservableCollection<> in its ViewModel:
<DataGrid ItemsSource="{Binding Path=Data}" SelectedItem="{Binding Path=CurrentItem}" />
ViewModel:
public ObservableCollection<TestModel> Data { get; set; }
private TestModel _currentItem;
public TestModel CurrentItem
{
get { return _currentItem; }
set
{
_currentItem = value;
RaisePropertyChanged("CurrentItem");
}
}
Now what I want is, that the DataGrid will preselect the first Row right on Form-startup. So I put the following in my test-code inside the constructor:
Data = new ObservableCollection<TestModel>
{
new TestModel() { Property1 = Guid.NewGuid().ToString() },
new TestModel() { Property1 = Guid.NewGuid().ToString() },
new TestModel() { Property1 = Guid.NewGuid().ToString() }
};
CurrentItem = Data[0];
The data is displayed but the first row isn't selected by the grid. Even if I set the binding to TwoWay, it won't work.
If I remove the SelectedItem-binding in XAML and add the following in Code-behind, it works well:
private void Window_Loaded(object sender, RoutedEventArgs e)
{
var m = this.DataContext as MainViewModel;
grid.SelectedItem = m.CurrentItem;
}
What's happening is that your VM is being assigned to the data context before the window is initialized and therefore never receives the message that the CurrentItem has changed because it was changed before it loaded.
What I do, is pass in the view model into View's constructor and set it after the InitializeComponent() function is called. Because I am using Prism I am using inversion of control (IOC) and Prism knows to input my VM into the constructor. If you are instantiating your view and view model yourself, you can just pass in the view model. I ran into the same issue and this works.
public MyView(IMyVM viewModel)
{
InitializeComponent();
this.DataContext = viewModel;
}
By the way, in working with MVVM, I see no reason not to pass in the ViewModel into the View because the view is dependent on it anyway. I know some people feel differently but it is either this or you will have to so some type of refresh of the datacontext in the Window_Loaded event.

Categories