Preselection in WPF-DataGrid with MVVM - c#

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.

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.

Binding ScrollView Content

I want to dynamically bind ScrollView content form ViewModel class.
I have xaml code:
<ScrollView
Grid.Column="1"
Content="{Binding UserGrid}"
HorizontalScrollBarVisibility="Never"
IsEnabled="False"
Orientation="Horizontal">
</ScrollView>
and ViewModel code
class ViewModel : BaseViewModel{
private Grid _userGrid { get; set; }
public Grid UserGrid
{
get { return _userGrid; }
set
{
_userGrid = value;
OnPropertyChanged();
}
}
public override async Task InitAsync()
{
await Task.Factory.StartNew(() =>
{
_userGrid = new Grid
{
ColumnSpacing = 1,
HorizontalOptions = LayoutOptions.FillAndExpand,
VerticalOptions = LayoutOptions.FillAndExpand
};
_userGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(GridHeightSize * 4) });
// and adding some content to This grid
}
}
Of course, I have initialized Binding context in View class
My question is if it is possible to bind this dynamically using this content binding.
Currently, I am getting an error:
,, No property, bindable property, or event found for 'Content', or mismatching type between value and property"
If it is not a proper way of dynamic view binding than what is?
The whole idea of databinding is to separate UI from data. Therefore, a data model should never contain any UI elements.
What you can do:
Create a new model class, which contains all the data you want to display inside that scroll view, for further reference i call it "CardViewModel" .
Add a property of List CardViewData to your ViewModel
Delete the InitAsync Task from your ViewModel
Make sure both of the view models implement the INotifyPropertyChanged interface
Now in the view consuming your ViewModel, overload the constructor to accept your view model as parameter.
Within the constructor you iterate through the items in the CardViewModel list and create a Grid for each item.
public TimeTableView(ViewModel model)
{
foreach (CardViewModel cardModel in CardViewData)
{
CardView view = new CardView(cardModel);
Container.Children.Add(view);
}
}
Note that "Container" is a placeholder for the element containing the cardviews.
Actually I would suggest that you implement a custom view, which contains all the Elements in order to display the data of your CardViewModel so you can set the binding context of the custom view to your cardview model:
public class CardView : Grid
{
public CardView(CardViewModel model)
{
BindingContext = model;
}
}
Also a ScrollView can only contain one element, so you can't simply throw a bunch of elements into it.
You probably need to put a grid inside it and then add the CardViews to that grid.

Binding combobox - WPF & C#

I have read loads of posts about this topic, but I cannot for the life of me figure this out, so your help is appreciated as I am losing the will to live!
I am trying to bind a list to a combobox in WPF, here is my code:
ViewModel:
public class ViewModelAddRegion
{
public List<DataAccessLayer.Model.CountryList> CountryList { get; set; }
public object GetCountryList()
{
List<DataAccessLayer.Model.CountryList> CountryList = new List<DataAccessLayer.Model.CountryList>();
CountryList = Data.DatabaseGets.GetAllCountries();
return CountryList;
}
}
So that gets my list. In the backing to my window, the code is:
public AddRegion()
{
var vm = new WineDatabase.ViewModel.ViewModelAddRegion();
var CountryAllList = vm.GetCountryList();
DataContext = CountryAllList;
InitializeComponent();
}
And finally, in my window:
<ComboBox Name="CountryList"
Margin="159,0,-160,0"
Grid.Row="1"
Grid.RowSpan="2"
ItemsSource="{Binding CountryAllList}"
DisplayMemberPath="CountryName"/>
Debugging, my list is populated as expected, but the combobox is forever empty.
Thanks for any assistance at all!
CountryAllList is just a local variable that you can't bind to. See the Data Binding Overview article on MSDN for details.
You should assign the ViewModel instance to the DataContext
var vm = new WineDatabase.ViewModel.ViewModelAddRegion();
vm.CountryList = vm.GetCountryList();
DataContext = vm;
and bind to its CountryList property
<ComboBox ItemsSource="{Binding CountryList}" ... />
Finally, in your GetCountryList method, it doesn't make much sense to assign the return value of Data.DatabaseGets.GetAllCountries() to a local variable. You could instead directly return it from the method.
public List<DataAccessLayer.Model.CountryList> GetCountryList()
{
return Data.DatabaseGets.GetAllCountries();
}
The GetCountryList() method may as well directly assign to the CountryList property
public void GetCountryList()
{
CountryList = Data.DatabaseGets.GetAllCountries();
}
and you could write the initialization code like this:
var vm = new WineDatabase.ViewModel.ViewModelAddRegion();
vm.GetCountryList();
DataContext = vm;
Change AddRegion method to:
public AddRegion()
{
var vm = new WineDatabase.ViewModel.ViewModelAddRegion();
vm.CountryList = vm.GetCountryList();
DataContext = vm;
InitializeComponent();
}
And in ComboBox set ItemsSource="{Binding CountryList}"

Hybrid MVVM implementation for a PropertyGrid (in DevExpress)

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;

Categories