I'm currently making a stock control app for Android with Xamarin.Forms.
I have an extra page just for entering the Pruduct ID. I want to use that page in two scenarions:
You pick an product to add/remove them from stock
You pick an product to tell the office that new ones have to be ordered
After picking the product you will be redirectet to:
The "DetailStockmovementPage" in case 1
The "DetailOrderPage" in case 2
The Problem is, that the next Page depends on what button was clicked before.
In my ViewModel I have the following Code to navigate (for scenario 1):
public async Task GoToNextPage(Product product)
{
await Navigation.PushAsync(new DetailStockmovementPage(product), true);
}
How can I make the next Page variable? Can I define a variable "NextPage" in my ViewModel, set it from the View like "vm.NextPage = DetailStockmovementPage" and Navigate like
public async Task GoToNextPage(Product product)
{
await Navigation.PushAsync(new NextPage(product), true);
}
I know that does not work, but I think that poor try made clear what I want to achieve. I mean I could push a string for each page and make an if-query before navigating, but I don't think thats an good way to do it. How can I vary the page to be navigated to?
You could pass the user's choice from the first page into the page where they select the product, and then use that information to decide which page to navigate to. For example:
In your App.cs file add an enum:
public enum NavTarget
{
Order,
Stockmovement
}
Define a property in your VM for the target selected in your menu page:
public NavTarget Target { get; set; }
...and use it in your navigation method:
public async Task GoToNextPage(Product product)
{
if (Target == NavTarget.Stockmovement)
await Navigation.PushAsync(new DetailStockmovementPage(product), true);
else
await Navigation.PushAsync(new WhateverItsCalledPage(product), true);
}
Then set that property in the constructor for your ProductSelectionPage:
public ProductSelectionPage(NavTarget target)
{
InitializeComponent();
// some code that sets the VM for this page
// ...
vm.Target = target;
}
Related
I have a view in my project with 2 nested views/components (the main view contains a tree view and the tree view contains individual 'node' views for each object in the tree).
I'm using event callbacks to get the clicked object's ID property (an int) back up to the top-tier view to let the main view know which level has been clicked, but the problem comes up in the last view. When I debug it, the onclick method seems to change the LevelID to 0, regardless of what it is before the method executes.
<DxTreeViewNode Text="#Level.LevelName" #onclick="#(async () => await SelectedLevelID.InvokeAsync(LevelID))">
#code {
private LevelInfo Level { get; set; }
[Parameter]
public int LevelID { get; set; }
protected override async Task OnInitializedAsync()
{
Level = await nodeViewModel.GetLevelInfoFromIDAsync(LevelID);
}
[Parameter]
public EventCallback<int> SelectedLevelID { get; set; }
}
If I hard code the value (e.g. InvokeAsync(1)), then it works as expected and clicking any level shows the data from level 1, otherwise it sets the views LevelID property to 0. I don't know if this is a bug or if I'm not using the callback properly. Thanks for any help :)
The problem was indeed that the click event was propagating so clicking a nested item triggered its own event, followed by the event of its parent, then its grandparent... There is no current way in Blazor to stop back propogation of events, but it is planned for a future release. I might get around it with a method which checks a bool is true and changes it to false so that only a single event callback gets triggered successfully, then reset that bool in the parent view, but that's a lot of faff.
I have a TabbedPage which shows deliveries in progress and finished deliveries. The model for both views is the same, only the service method from where we get the data is different, so I would like to reuse the ViewModel.
Would it be a good solution to reuse the ViewModel by passing some navigation data into my InitializeAsync method that would allow me to decide which service method to use to get the data for the view?
I would override OnCurrentPageChanged in TabbedPage View's code-behind and Initialize the ViewModel from there
TabbedPageView.xaml.cs
protected override async void OnCurrentPageChanged()
{
base.OnCurrentPageChanged();
if (!(CurrentPage.BindingContext is TabbedPageViewModel tabbedPageViewModel)) return;
if (CurrentPage == DeliveriesInProgress)
{
await tabbedPageViewModel.InitializeAsync("DeliveriesInProgress");
}
else if (CurrentPage == FinishedDeliveries)
{
await tabbedPageViewModel.InitializeAsync("FinishedDeliveries");
}
}
TabbedPageViewModel.cs
public async Task InitializeAsync(object navigationData)
{
if (navigationData is string deliveryType)
{
if (deliveryType == "InProgress")
{
Deliveries = await _deliveryService.GetDeliveriesInProgress();
}
else if (deliveryType == "Finished")
{
Deliveries = await _deliveryService.GetFinishedDeliveries();
}
}
}
What could be alternative solutions?
The best way is to use two different properties in your viewmodel. Then you can bind the two different views in the tabs to the associated property.
In your viewmodel:
public ObservableCollection<MyDeliveryModel> FinishedDeliveries;
public ObservableCollection<MyDeliveryModel> DeliveriesInProgress;
Know you can add two methods to load the data for those properties:
public async Task RefreshFinishedAsync()
{
// Your logic to load the data from the service
}
public async Task RefreshInProgressAsync()
{
// Your logic to load the data from the service
}
And then in your TabbedPage-Event:
if (CurrentPage == DeliveriesInProgress)
{
await tabbedPageViewModel.RefreshInProgressAsync();
}
else if (CurrentPage == FinishedDeliveries)
{
await tabbedPageViewModel.RefreshFinishedAsync();
}
With this solution you can separate the data and you don't need to reload the whole data everytime you change the tabs. You can check if there is already some data in the collection, and if so... just don't reload the data. Just do it if the user wants it.
This improves the performance and the "wait-time" for the user.
Or as an alternative:
Load all data at once and just filter the data for the two collection-properties. This reduces the service-calls.
You can accomplish this by using a base viewmodel and a view model for each tab that uses the base. The base then holds your commands and deliveries. you bind each tabbed page to the viewmodel for that page so you won't need to check on tab changed. When you construct each viewmodel, pass in the information needed to base to know how to query the data. For each tabbed view, if the views are the same for in progress and finished, use a partial view and put it in both tabbed pages. This gives flexibility in the long run.
public class InProgressDeliveriesViewModel: BaseDeliveryViewModel{
public InProgressDeliveriesViewModel():base(filterParams){}
}
public class FinishedDeliveriesViewModel: BaseDeliveryViewModel{
public FinishedDeliveriesViewModel():base(filterParams){}
}
public class BaseDeliveryViewModel{
private FilterObjectOfSomeSort _filterParams;
public BaseDeliveryViewModel(filterParams whatever){
//use these params to filter for api calls, data. If you are calling the same
//endpoint pass up the filter
_filterParams = whatever;
}
public ObservableCollection<MyDeliveryModel> Deliveries {get;set;}
public async Task LoadDeliveries(){
//use the filter params to load correct data
var deliveries = await apiClient.GetDeliveries(filterParams); //however you
//are gathering data
}
.... All of your other commands
}
I am working on a Windows Phone 7 application. Now I need to switch the view after a user tapped the designated button which takes user to another view.
Which component, theoretically, in MVVM should be in charge of the navigation, i.e. switching views? Code snippets would be good to show demonstration.
I have tried inserting the switching code in View and it works alright, but I encountered a situation where I call an asynchronous web service and would like to navigate user to the new view only after the operation is done, the navigation code should be inside the event handler.
Thank you.
P/S: My project's deadline is coming soon, I have no time to rebuild my project using MVVM tools, such as MVVM Light, Caliburn Micro, and etc.
I put a Navigate methods in the base class that all my ViewModel's share:
protected void Navigate(string address)
{
if (string.IsNullOrEmpty(address))
return;
Uri uri = new Uri(address, UriKind.Relative);
Debug.Assert(App.Current.RootVisual is PhoneApplicationFrame);
BeginInvoke(() =>
((PhoneApplicationFrame)App.Current.RootVisual).Navigate(uri));
}
protected void Navigate(string page, AppViewModel vm)
{
// this little bit adds the viewmodel to a static dictionary
// and then a reference to the key to the new page so that pages can
// be bound to arbitrary viewmodels based on runtime logic
string key = vm.GetHashCode().ToString();
ViewModelLocator.ViewModels[key] = vm;
Navigate(string.Format("{0}?vm={1}", page, key));
}
protected void GoBack()
{
var frame = (PhoneApplicationFrame)App.Current.RootVisual;
if (frame.CanGoBack)
frame.GoBack();
}
So the ViewModel base class executes the navigation if that's what you are asking. And then typically some derived ViewModel class controls the target of the navigation in response to the execution of an ICommand bound to a button or hyperlink in the View.
protected SelectableItemViewModel(T item)
{
Item = item;
SelectItemCommand = new RelayCommand(SelectItem);
}
public T Item { get; private set; }
public RelayCommand SelectItemCommand { get; private set; }
protected override void SelectItem()
{
base.SelectItem();
Navigate(Item.DetailPageName, Item);
}
So the View only knows when a navigate action is needed and the ViewModels know where to go (based on ViewModel and Model state) and how to get there.
The view should have a limited number of possible destinations. If you have to have a top-level navigation on every page, that should be part of your layout or you can put them in a child view.
I put navigation outside of MVVM in a class that is responsible for showing/hiding views.
The ViewModels use a messagebroker with weakevents to publish messages to this class.
This setup gives me most freedom and doesn't put any responsibilities in the MVVM classes that do not belong there.
In Orchard CMS, I can create a part and weld it to the site using Filters.Add(new ActivatingFilter<TermsAndConditionSettingsPart>("Site")); and have the editor for this part show up in the site settings.
I also have a few pages in my admin screens that I have used controllers and actions to allow the user edit settings for my modules.
I am wondering how I can weld a part onto one of my custom admin pages. I think I need to do something similar to the code mentioned above, but I'm not too sure what I should be welding to (ie- what should I replace "Site" with)? Do I need to create a content type for each of my admin pages?
Any help would be appreciated.
After exploring further and taking into account #Piotr's excellent answer, I've managed to achieve what I wanted to do.
Step 1: Migrations
private readonly IOrchardServices _services;
public Migrations(IOrchardServices services) {
_services = services;
}
public int Create()
{
ContentDefinitionManager.AlterTypeDefinition("PlayerSearch", cfg => { });
var content = _services.ContentManager.New("PlayerSearch");
_services.ContentManager.Create(content);
return 1;
}
In the example above, "PlayerSearch" is the name of my content type (ie the item I will be welding my parts to). This code simply creates a PlayerSearch type and creates a single instance of it which is then persisted.
Step 2: ContentPart
I created a simple POCO class as a ContentPart. This is what I want to weld to my PlayerSearch page:
public class PlayerSearchPart : ContentPart
{
public string String1 { get; set; }
public string String2 { get; set; }
public string String3 { get; set; }
public int Int1 { get; set; }
public int Int2 { get; set; }
public int Int3 { get; set; }
}
Step 3: ContentHandler
The next thing I did, was to weld my PlayerSearchPart to my PlayerSearch type as defined in the migrations:
public class PlayerSearchHandler : ContentHandler
{
public PlayerSearchHandler()
{
Filters.Add(new ActivatingFilter<PlayerSearchPart>("PlayerSearch"));
}
}
I did this in the ContentHandler by using the ActivatingFilter.
Step 4: The Controller
Now we need to create a page in the admin screens that is capable of displaying all the welded parts to the user:
private readonly IOrchardServices _services;
public PlayerManagementController(IOrchardServices services) {
_services = services;
}
[HttpGet]
public ActionResult PlayerSearch()
{
var playerSearchType = _services.ContentManager.Query().ForType(new[] {"PlayerSearch"}).Slice(0, 1).FirstOrDefault();
var model = _services.ContentManager.BuildEditor(playerSearchType);
// Casting to avoid invalid (under medium trust) reflection over the protected View method and force a static invocation.
return View((object)model);
}
This action retrieves the instance of my PlayerSearch type that I created in the Migrations file, builds the editor for it and then passes this to the view.
Step 5: The View
Our action of course needs a view, and it's pretty simple once you know how:
#using (Html.BeginFormAntiForgeryPost()) {
#Html.ValidationSummary()
#Display(Model.Content)
<fieldset>
<button class="primaryAction" type="submit">#T("Save")</button>
</fieldset>
}
Step 6: The Driver
Nothing out of the ordinary here:
// GET
protected override DriverResult Editor(PlayerSearchPart part, dynamic shapeHelper)
{
return ContentShape("Parts_PlayerSearch_Edit",
() => shapeHelper.EditorTemplate(
TemplateName: "Parts/PlayerSearch",
Model: part,
Prefix: Prefix));
}
// POST
protected override DriverResult Editor(PlayerSearchPart part, IUpdateModel updater, dynamic shapeHelper)
{
updater.TryUpdateModel(part, Prefix, null, null);
return Editor(part, shapeHelper);
}
Step 7: Displaying the editor for your part
The final step here is to create the editor template for your part, and to also ensure that you have a record in your placement.info for the part so that Orchard knows where to render it.
This technique was taken from the way that the site settings work. To have a look at this, and to see how the Post action would work too, have a look at the controller at Orchard.Core.Settings.Controllers.AdminController.
You cannot weld anything onto a custom page - it doesn't work like this.
Parts are welded onto content items, like Site. Site is no different from other content items, like User, Page etc., with one exception - there is always a single Site item per tenant. What you actually see when you go to any section in Settings is an editor for that item (and each section displays a part of that editor that corresponds to a named group). And this functionality is specific for Site items only.
If you need site-wide settings, the best way is to always weld parts onto Site item. Then you can provide links (or tabs etc.) that point to that specific editor from inside your custom views.
MonotouchDialog makes it very easy to create UITableView Dialogs, but sometimes questions like that one popup:
MonoTouch Dialog. Buttons with the Elements API
Now, I have a similar problem but quite different:
List<User> users = GetUsers();
var root =
new RootElement ("LoginScreen"){
new Section ("Enter your credentials") {
foreach(var user in users)
new StyledStringElement (user.Name, ()=> {
// tap on an element (but which one exactly?)
}
),
}
navigation.PushViewController (new MainController (root), true);
Now, the second parameter of StyledStringElement's constructor has the type of NSAction delegate, and doesn't take any arguments, now I dunno how to determine exactly which element been tapped.
How to get that?
If it was Tapped then it has been selected. So you should be able to inherit from StyleStringElement and override its Selected method to accomplish the same goal.
e.g.
class UserElement : StyleStingElement {
public UserElement (User user) { ... }
public override Selected (...)
{
// do your processing on 'user'
base.Selected (dvc, tableView, indexPath);
}
}
For Touch.Unit I created a new *Element for every item I had, TestSuiteElement, TestCaseElement, TestResultElement... to be able to customize each of them and adapt (a bit) their behaviour but I did not use this Selected to replace Tapped. You might want to check but it would not fit with your code pattern to create elements.
"...a Flower by any other name?"
If you look closely NSAction's are just delegates. I prefer to pass Action / Func into those params the reference for which is contained within the...container controller.
So lets so you have a UINavigationController that pushes a DialogViewController. When your element is selected you provide the unique user that you've passed to the Element and go from there :-)
public class MyNavController : UINavigationController
{
Action<User> UserClickedAction;
public MyNavController()
{
UserClickedAction = HandleUserClicked;
}
public void HandleUserClicked(User user)
{
...
}
}