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.
Related
This's my first question here, so hi everybody.
I'm working on the mobile app in Xamarin.Forms with Prism. I've created ListView where shown data from the database.
When the user clicks in the selected row app should navigate to a new view and pass the selected item from ListView.
<ListView x:Name="DefectsBase"
RowHeight="65"
ItemsSource="{Binding Defects}"
ItemSelected="ShowDetailsEvent"
IsPullToRefreshEnabled="true"
RefreshCommand="{Binding Refresh}"
IsRefreshing="{Binding IsRefreshing}">
Code backend:
async void ShowDetailsEvent(object sender, EventArgs e)
{
var myListView = (ListView)sender;
var myItem = myListView.SelectedItem;
var p = new NavigationParameters();
p.Add("selectedDefect", myItem);
await _navigationService.NavigateAsync("DefectDetailsView", p);
}
Unfortunately, the app doesn't respond to pressing the selected row in ListView.
As I can see you are already using Prism and you have a List page with Items and you want to navigate to some details page based on the selected/taped/chosen item which the user taps in the ListView.
The idea is to move as much code and logic as we can to the view model and keep our code-behind. This is pretty easy to solve using Prism and EventToCommand behaviour.
In the example and answer below, I will show you how to solve this with few lines of code, with a nice code approach.
First of all, I recommend you use EventToCommand behaviour, you can include it with prism xmlns, like this: xmlns:prism="http://prismlibrary.com", later on, you can use it with ListView.
Remove ItemSelected event from your ListView and move the markup about it to the <ListView.Behaviors> part. Here is my code sample for the ListView which binds to some ObserverableCollection of the Car models:
<ListView ItemsSource="{Binding Cars}">
<ListView.ItemTemplate>
<DataTemplate>
...
</DataTemplate>
</ListView.ItemTemplate>
<ListView.Behaviors>
<prism:EventToCommandBehavior EventName="ItemTapped"
Command="{Binding SelectedCarCommand}"
EventArgsParameterPath="Item" />
</ListView.Behaviors>
The main part here is <ListView.Behaviors>, where you can see that I am binding to the SelectedCarCommand which will be invoked when the user taps on some of the items from the list. I am using the ItemTapped event for this and passing the current "taped" item from the list as a parameter.
In order to follow this XAML part in my view model of this page, I have declared the DelegateCommand and method which will be called when the command is invoked. The view model part looks like this:
This is my CarListPageViewModel, take a look at DelegateCommand and SelectedCar method.
public class CarListPageViewModel
{
private readonly INavigationService _navigationService;
public ObservableCollection<Car> Cars { get; set; }
public DelegateCommand<Car> SelectedCarCommand { get; private set; }
public CarListPageViewModel(INavigationService navigationService, IDataProvider dataProvider)
{
_navigationService = navigationService;
// Insert test data into collection of Cars
Cars = new ObservableCollection<Car>(dataProvider.GetData());
SelectedCarCommand = new DelegateCommand<Car>(SelectedCar);
}
private async void SelectedCar(Car selectedCar)
{
NavigationParameters navigationParameters = new NavigationParameters
{
{ "selectedCar", selectedCar }
};
await _navigationService.NavigateAsync(nameof(CarDetailsPage), navigationParameters);
}
}
As you can see we have DelegateCommand defined with the type of parameter which will be passed, in my case, this is the Car class, the same class as our items in the ListView.
In the constructor, I did my initialization and defined the method which will be called, that method has a parameter of the type Car.
When the user taps on one of the items in the ListView, SelectedCar (method) will be called and we can pass the data to the next view using NavigationParameters and NavigationService.
In order to retrieve the passed data we can use INavigationAware in the details view model and with the OnNavigatedTo method, access the data which is being passed.
This is my CarDetailsPageViewModel, take a look at OnNavigatedTo method.
public class CarDetailsPageViewModel : BindableBase, INavigationAware
{
private string carTitle;
public string CarTitle
{
get { return carTitle; }
set { SetProperty(ref carTitle, value); }
}
private string photoUrl;
public string PhotoUrl
{
get { return photoUrl; }
set { SetProperty(ref photoUrl, value); }
}
public CarDetailsPageViewModel() { }
public void OnNavigatedTo(INavigationParameters parameters)
{
if (parameters.ContainsKey("selectedCar"))
{
Car car = parameters.GetValue<Car>("selectedCar");
if (car != null)
{
CarTitle = $"{car.Make} {car.Model}";
PhotoUrl = car.PhotoUrl;
}
}
}
public void OnNavigatedFrom(INavigationParameters parameters) { }
}
From this answer and example, you can see:
How to, use EventToCommand behaviour with ListView
Define and use DelegateCommand with passing parameter
How to navigate to another view and pass navigation parameter and
... finally how to access the passed data.
Code and this sample you can find on my GitHub profile here.
Hope this answer was helpful for you!
Wishing you lots of luck with coding! 👋
I'm using MVVM in a Xamarin application, I have an interface to navigate between pages:
public interface INavigate
{
INavigate Next();
INavigate Previous();
string ViewTitle { get; }
}
In the implementing views:
public partial class V2Upload : ContentView, INavigate
{
public string ViewTitle => "Upload photos";
public INavigate Next()
=> new V3AdDetail();
public INavigate Previous()
=> new V1Agreement();
}
and in the view model
I have a property of type INavigate:
public INavigate CurrentAddItemStep
{
get { return _currentAddItemStep; }
set { Set(ref _currentAddItemStep, value); }
}
and the Content property of the parent view is bound to this property:
when next button is clicked I execute this code:
CurrentAddItemStep = CurrentAddItemStep.Next();
ViewTitle = CurrentAddItemStep.ViewTitle;
now a validation method is required before navigating to the next page for all the Content views..
I want to keep the MVVM pattern as clean as possible by not writing business code in the view, for example in the V2Upload view the File1 and File2 properties of the view model shouldn't be null:
private bool ValidateFiles(){
return (File1 ?? File2) != null;
}
but since the navigating is done dynamically in run-time, I can't know which view is the current view.
I'm thinking to use reflection , to know what is the name of the view (but this will break the whole design)
Another option is to provide a function parameter to the Next method, but also how to provide it in the design time from the view model?
This is what I'm doing now:
public INavigate Next()
{
if (((ViewModel.AddItemViewModel)BindingContext).ValidateFiles())
return new V3AdDetail();
else
return this;
}
but again, I'm accessing the view model from the view (and had to change the ValidateFiles method from private to public), which I want to avoid
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;
Currently When I press on buttons they produce New pages and creates a new Tab at the Top. I'm trying to make a case where If the tab is already created it be redirected to the working one. May I get some tips or guidance please.
public void Show(string name)
{
IGridPort tab;
switch (name)
{
case "Contacts": tab = new ContactsGridViewModel(Events); break;
case "Businesses": tab = new ClientGridViewModel(Events); break;
default: tab = new QuickLaunchViewModel(Events); break;
}
Events.Publish(new ShowTabEvent(tab));
}
In my apps I typically have a base class called PageViewModel for each of the tabs on the main page and a derived class called DataPageViewModel for displaying pages that should only appear once (e.g. a record that's being edited). I maintain an observable collection of PageViewModel to pass as the ItemsSource into my tab control and I also maintain a dictionary of DataPageViewModel so I can look them up based on the data they are displaying:
public ObservableCollection<PageViewModel> Pages { get; private set; }
private Dictionary<string, DataPageViewModel> DataPages { get; set; }
The string that I use to key the dictionary is generally a combination of the page type and a unique identifier for the data being displayed. All that remains then is to check the dictionary before you create a page to see if another page displaying that data already exists, if it does then just set that page as the active one. Setting the active page can be done by getting the DefaultView of the ObservableCollection and calling MoveCurrentTo, but a better method in MVVM is to create a property in your model to hold the currently active page:
private PageViewModel _CurrentPage;
public PageViewModel CurrentPage
{
get { return _CurrentPage; }
set { _CurrentPage = value; RaisePropertyChanged(() => this.CurrentPage); }
}
Then just bind it to SelectedItem in your tab control:
<TabControl
ItemsSource="{Binding Pages}"
SelectedItem="{Binding CurrentPage, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
IsSynchronizedWithCurrentItem="True" />
The binding is two-way so if the user selects a tab then CurrentPage will be updated accordingly, and if you set CurrentPage in your ViewModel then the corresponding tab will be selected in the View.
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.