MAUI with MVVM - How Do I implement a Submit method in a 'details' page? - mvvm

I am new to MVVM and MAUI, and I am quite a bit confused now.
This is the situation - I will explain it as well as I can:
In my main page I have a CollectionView with frames etc., and in each frame I show a picture of some CAR object. On clicking on a frame I execute the command
[RelayCommand]
private async Task GoToCarsDetails(Car car)
{
if (car == null)
return;
await Shell.Current.GoToAsync(nameof(CarPage), true, new Dictionary<string, object>
{
{"Car", car}
});
}
In my CarPage.xaml, which I reach with the method above, I have a grid which contains this xaml:
<Label
x:Name="Description"
Grid.Row="2"
Style="{StaticResource MediumLabelCentered}"
Text="{Binding Car.Description, StringFormat='Description: {0}'}" />
<Label
x:Name="Color"
Grid.Row="3"
Style="{StaticResource MediumLabelCentered}"
Text="{Binding Car.Color, StringFormat='Color: {0}'}" />
and also an Editor to be able to change the Description, a Picker to be able to change the Color, and a Submit button.
I want to allow the user to set some description and a color, and this should NOT immediately change the CAR object passed as param to the method Shell.Current.GoToAsync - they should only be changed when clicking on the submit button - if the user changes for instance the color from the initial Red to Blue, but does NOT click the Submit button and goes back to the previous screen (Main) instead, the property Color should NOT have changed - it should still show "Red".
So, for instance, let's say that for some car I have the
Color = RED, and
Description = "My Car"
Click on that car, go to the CarPage.xaml, see in the Picker the color set to RED and the description in the editor = "My Car", change those to BLUE and "Your Car", respectively, and navigate away without clicking on the Submit button. The initial car should still show
Color = RED, and
Description = "My Car"
(no change).
Do the same thing as above but this time click on the submit button. This should take us to the previous page (Main), and see that the initial car now shows
Description = "Your Car", and
Color = BLUE
My problem is this:
If I BIND the controls, as seen in the xaml above, then any change on those Editor and Picker will already change the underlying Car object, and then the Submit button has no meaning anymore. If I click on it or not, that makes no difference - the Car object already contains those changes.
If instead of opening the CarPage.xaml page with the Car object I open it with a clone, with the intention of allowing the user wo freely play on the clone object, by passing a clone object
car.Clone<Car>()
(I have created an extension method for that). I can now indeed change the Clone's properties all I want, but then how do I replace on Submit the object CAR with the object CARCLONE ? The code for that Submit button exists either in the code-behind of the CarPage.xaml object, either in the CarViewModel class. In neither object do I have access to both CAR and CARCLONE, to be able to switch them.
I cannot switch these two objects (the original CAR object with the changed CARCLONE object) in the MainViewModel's GoToCarsDetails method
private async Task GoToCarsDetails(Car car)
either, because this method calls Shell.Current.GoToAsync in an async way, and I cannot add the code which would switch objects after the Shell.Current.GoToAsync call, because the code will not wait for the CarPage to be closed.
I also thought of passing the real CAR object as param, NOT binding the two labels above, play with the controls, and populate the CAR object with the updated properties only in Submit_Click().... But how do I set, on opening the CarPage page, the Text of the two labels ? (Is there a Load() event ? I couldn't find one). Writing such code in the constructor does not work (it gives me a null reference).
So... I don't know how do do this seemingly very simple task:
Pass an object to a Details form, allow it to be changed, but actually update it to our collection in Main only if the user has clicked on the submit button in that Details page (CarPage, that is), all by not violating the MVVM ideas.
Thank you very much.
Alex

There is a standard mechanism, in any language, for dealing with situations like this: a "callback".
Think about "Responsibilities". The main page (and its viewmodel) is responsible for managing the Car objects.
You want somewhere else to work with a Car object, but that "somewhere else" [I'll call it CarEditor] isn't the place that should deal with the consequences of modifying a Car.
So what do you want to pass in? A Car. What do you want to do on Submit_Click? Something that CarEditor doesn't need to know about. It should hand all the information "back" (via the callback action) to somewhere that has the responsibility to act on the result.
As Jason suggested, one way to do this is to have a clone of Car. Then pass back both original and clone. This could be done as (Car original, Car clone).
Calling code:
Car original = ...;
Editor.EditThis(original, ProcessTheEdit);
...
void ProcessTheEdit(Car original, Car clone)
{
}
Editor code:
public delegate void CallbackDlg(Car original, Car clone);
public void EditThis(Car original, CallbackDlg callback)
{
Car clone = new Car();
//... do work.
callback(original, clone);
}

I have followed your advice, and here it is:
[RelayCommand]
private async Task GoToCarsDetails(Car car)
{
Car carClone = car.Clone();
await Shell.Current.GoToAsync(nameof(CarPage), true, new Dictionary<string, object>
{
{"Car", car},
{"CarClone", carClone}
});
}
CarPage has the controls bound to CarClone.
On the Submit button in the ViewModel I have:
[RelayCommand]
private void Submit()
{
Car = carClone.Clone();
Shell.Current.GoToAsync("../", true);
}
If I change the color from the initial RED to YELLOW and I set a breakpoint on the Shell line, I see that indeed Car now has the color property = YELLOW.
Good.
However, if I click again on the same car, the CarPage view opens again with Color = RED, which is not correct. I have changed that Car object to have the color set to Yellow, right ? The Cars collection in this MainViewModel shows as unchanged !
I don't understand - I have passed the Car object to the "Details" CarView page and I have changed it from Red to Yellow, and yet... it is still Red. It's like I have passed the Car object Byval and not Byref.... :-(
Thank you,
Alex

Related

Unity - How to react to scene picking? How to force select a parent by picking its child in the sceneview

I have the following situation I need an answer to:
I have a parent object with children. These children all have unique meshes. Whenever these children are selected in the SceneView, the parent needs to be selected in stead. The children should never have their inspectors exposed (not even a fraction of a second).
How do I do this?
There are two possible solutions that I have come up with which are not the solution I wish to go for.
First being using [SelectionBase] attribute on the parent object. This work perfectly, but only once. The first time the child is selected, the parent gets selected. However, when I click the child again, it still gets selected.
Second solution was to react on Selecten.onSelectionChanged. This however is too slow. I can set the selection to the parent if a child gets selected, but the child gets exposed for a few frames still.
Is there an instant solution to this which can guarantee me the functionality of SelectionBase, but then every time in stead of only the first time I click it?
Thanks in advance! :)
I have found a way to do exactly what i want. I combine the [SelectionBase] attribute with a piece of code in the editor OnSceneGui.
First add the [SelectionBase] attribute to your class
Second add this code to its editor class
private void OnSceneGUI()
{
HandleUtility.AddDefaultControl(0);
//Get the transform of the component with the selection base attribute
Transform selectionBaseTransform = component.transform;
//Detect mouse events
if (Event.current.type == EventType.MouseDown)
{
//get picked object at mouse position
GameObject pickedObject = HandleUtility.PickGameObject(Event.current.mousePosition, true);
//If selected null or a non child of the component gameobject
if (pickedObject == null || !pickedObject.transform.IsChildOf(selectionBaseTransform))
{
//Set selection to the picked object
Selection.activeObject = pickedObject;
}
}
}
This allows the first pick to select the component. From then on, only when you select non-child objects in the scene, selection will actually change.

Unity UI : How to reenable a dropdown after its panel was deactivated and then reactivated?

I am new to Unity, so this is probably a dumb question but here goes:
I have a Unity app; it has several scenes. One of those scenes has a Canvas upon which there are several overlapping Panels. On one of the Panels (PanelA), there is a Dropdown. When the app first runs PanelA is active and on top by default and the dropdown works fine. But after I deactivate PanelA with a C# call to
panelA.SetActive(false);// hides the panel
and then later reactivate it like so:
panelA.SetActive(true); // redisplays the panel
The dropdown no longer displays its list of option when clicked on. The dropdown is still there and still shows its first option "None" and when clicked on the drop-down changes color to indicate that it was touched, but the dropdown does not drop down its list of options.
What am I missing? Why doesn't the dropdown show the list of options?
Thanks
Windows 10 Pro v1709, unity 2017.1.1f1 Personal
My hierarchy is like so, PanelB is the panel that gets set active after PanelA is inactivated:
The script called when the dropdowns option is selected is called like so in Start():
PickDropDown.onValueChanged.AddListener(delegate {
myDropdownValueChangedHandler(PickDropDown);
});
and the called function is:
private void myDropdownValueChangedHandler(Dropdown target) {
Debug.Log("selected: "+target.value);
stage = 0;
currentval = PickDropDown.options [PickDropDown.value].text;
advice.text = "";
title.text = currentval;
if (currentval != NONE) {
advice.text = "You chose to do "+currentval;
nextVal();
}
}
void newVal(){
Debug.Log("newVal " );
PanelA.SetActive (true);
PanelB.SetActive (false);
Debug.Log("options count="+ PickDropDown.options.Count );
}
void nextVal(){
Debug.Log("nextSet " );
PanelB.SetActive (true);
/*** next line shows the answer to my problem - from killer_mech **/
Destroy(PickDropDown.transform.Find("Dropdown List").gameObject);
/***************************************************************/
PanelA.SetActive (false);
}
Yes, this is a bug in unity. I can reproduce this now. This is happening because Unity is creating a game Object called "Dropdown List" inside dropdown which should get destroyed after the closing. But I have noticed a slight delay is happening while destroying(I used editor debug mode for this). When you set the game object active flag as false, it somehow stops the destroy object and results in object Dropdown List staying there so next time when you try to interact with it the object will still present. You will also notice a white box below the Dropdown list after this which indicates the dropdown list still is present.
For the solution what I did was just before I was about to turn my gameobject off I used
Destroy(PickDropDown.transform.Find("Dropdown List").gameObject);
Destroying this object solves the problem. I know this is not a good way to do it, but I checked whether any references to options are destroyed while destroying this which resulted in no error for me. And Dropdown creates new "Dropdown List" gameobject. So it is safe to assume that destroying is OK. Well not sure if any other good solution for this is out there but for now you can use this.
Note: I have tested this bug with Panel, Empty Gameobject as parent and finally with Dropdown
This shows what is happening inside the drop-down. 1) During No popup 2) During Open 3)After setactive off before the child is destroyed.

A panel to change 3d object properties when right click on it

I'm building a UI system to edit selected 3D objects. I have spheres on the screen and I want to be able to edit their proprieties (radius for example). In order to do that I want the user to be able to click on a sphere and then it show a panel next to the object. When the user changes slider value the radius of the sphere which as been clicked on change. Using the new unity event system, I think it's easy to achieve, but I'm a new to unity and even if I know the basic I don't know how to organize this properly.
At the end my goal would be to select multiple sphere, right click and edit the radius to all selected spheres.!
Any tips on how to do that ? Sorry for my poor english. Thanks
Here's how you can do it:
Keep a bool for tracking if the object is selected, say, bool _isSelected
Toggle this boolean in the OnMouseUpAsButton() function. This function is called when you click on an object and release that click on the same object. Unity's OnMouseXXX() interfaces are very good for handling mouse events. You just need to define this function in the sphere objects' script.
Put together a Slider and arrange its value interval from editor. Define a function that will edit the scale value of the spheres and tie that function to that slider.
Below you can see an example of how I did that in a game of mine.
Unity simply catches the slider's value if you provide it with a public void _f(float val). It will appear on the top of the list as "Dynamic float". So, in your case, your slider update function should look like this:
public void SphereScaleSliderUpdate(float val)
{
foreach (GameObject sphere in GameObject.FindGameObjectsWithTag("your tag here"))
{
if (sphere.IsSelected) // don't forget to provide a public getter for _isSphere variable
{
sphere.transform.localScale = new Vector3(val, val, val);
}
}
}
Also, you need to create another script for toggling the menu(Canvas/Panel(optional)) that contains the slider, and you may add it to another object, like a UIManager. Assign the Canvas in the game hierarchy to a variable in that script, and then scan for RClick in the Update() function of the UIManager.
.
void Update()
{
if (Input.GetMouseButtonDown(1) // RClick event
{
// makes the slider appear through enabling canvas
_canvasHandle.enabled = true;
}
}

Show/ hide Unity 3D elements

I hope I am posting in the correct section. I know that it is possible to write a custom code in Unity so I have the following questions:
Imagine a house model in Unity. Would it be possible to have a code which helps to hide/unhide certain objects? For example, letter "W" would hide/unhide all windows, letter "C" would hide/unhide all columns etc.
If it would be possible to develop a code for that, what would be the workflow? How would Unity know what is window and what is door?
Taking one step further.. Would it be possible to have a code that unhides the next step of the project. For example, the first step would be building foundations. Would it be possible to have a code that would unhide the next step, say, 1st floor floor element, with a klick of a keyboard key? And then with the same key unhide the next step which might be 1st floor walls. And would it be possible with another key go backwards?
If such code would be possible, what do you think would be the workflow? How would Unity know which element is in which step?
Yes just have a class with an array of objects to show/hide and another property for what button will do it. Then just have a method that will hide/unhide each object in that class. In the update method of a behavior just check the input and call the method of the class based on what button was pressed.
Would it be possible to have a code which helps to hide/unhide?
Yes, you can do that by calling GameObject.SetActive() function
door.SetActive(true); // door is a game object;
How would Unity know what is window and what is door?
You can give the window/door a name, then access the game object by name
var door = GameObject.Find("MyDoor") as GameObject;
Would it be possible to have a code that would show the next step, say, 1st floor floor element, with a klick of a keyboard key?
Yes, you can do that with code snippet below:
int step = 1;
void Update() {
if (Input.GetKeyDown("space")){
print("space key was pressed");
step++;
}
if(step == 1) {
// do sth
}
else if(step == 2) {
// do sth
}
}
Given the above snippet, your last answer can be easily deduced.
I see 2 ways of doing what you asked.
1st way
GameObject[] walls;
walls = GameObject.FindGameObjectsWithTag("Wall");
foreach (GameObject wall in walls)
{
wall.setActive(true);
}
What you will have to do, besides the code, is to assign to those object a correct tag (like "Wall" "Window" "WatheverYouWillNeed"). So you can easily find all objects by giving them some kind of order (with tags).
Best practice tip
You may want to create a static class "Tags", and set public constants string inside the class, with all tag names.
public static class Tags
{
public static const WINDOW_TAG = "Window";
// ... more tags
}
2nd way
If what you want to set is only "visibility", you may want to modify only camera tag rendering. It doesn't even require any cycle at all. When you want to view windows, you just tell your camera to render the tag "Window".
// Turns on Windows Layer on mask using an OR operation
private void Show()
{
camera.cullingMask |= 1 << LayerMask.NameToLayer("Window");
}
// Turn off the bit using an AND operation with the complement of the shifted int:
private void Hide()
{
camera.cullingMask &= ~(1 << LayerMask.NameToLayer("Window"));
}
It may look odd the second option, but keep in mind that the cullingMask is composed of bits, and every bit is a Layer defined by your tags. The "Everything" culling mask is 111111. If you want to see only Window, for example, and window is the last element, your culling mask would look something like 0000001
Hope it helped! :)

SIP prevents EF from auto-detecting object change on WP7

I have an issue trying to persist entities to the DB using code-first technique. For example of what I am doing you may look at the following MSDN Sample. The app generally works as intended except for one case.
If I have an existing entity and I bind it to a page that has a TextBox to hold the Title field and a AppBar icon to Save (similar to the 'New Task' screenshot in the above link, but with values pre-filled with existing entity with Two-Way binding), the following issue occurs. If I have the TextBox selected and I change the title and hit the save button, it updates the entity in-memory so that the full list now displays the new title. But the new title is not persisted to the DB (it does not auto-detect changes). This is weird, not just because the object in-memory has changed, but also because if I deselect the TextBox and then hit save, it will persist the changes to the DB.
I have seen other questions on SO with some change detection issues, they suggest adding this.Focus() or focusing some other element at the beginning of the save method. This does not help in my case. Unless I tap on screen to deselect the TextBox and hide the keyboard (or press Return key on the keyboard, which I bound to do this.Focus()), it won't detect the object as changed.
How can I address this? What exactly is stopping EF from detecting the object change when the keyboard is still visible?
Not sure if I follow exactly what you described but I think the problem is that the property you have bound your textbox to does not get updated until the TextChanged is fired on the textbox, and this is done first when you leave the Textbox, basically it will lose Focus if you tap somewhere else.
There is a simple workaround for this and it behaviors. By making a small behavior you can force the textbox to update the binding on each keystroke - so everything is updated while you type and keyboard is still there.
Behavior:
/// <summary>
/// Update property on every keystroke in a textbox
/// </summary>
public class UpdateTextSourceTriggerBehavior : Behavior<TextBox>
{
protected override void OnAttached()
{
this.AssociatedObject.TextChanged += OnTextBoxTextChanged;
}
void OnTextBoxTextChanged(object sender, TextChangedEventArgs e)
{
var bindingExpression = AssociatedObject.ReadLocalValue(TextBox.TextProperty) as BindingExpression;
if (bindingExpression != null)
{
bindingExpression.UpdateSource();
}
}
protected override void OnDetaching()
{
this.AssociatedObject.TextChanged -= OnTextBoxTextChanged;
}
}
Now just attach this behavior to your textbox like this:
<TextBox Text="{Binding YourPropertyName, Mode=TwoWay}">
<i:Interaction.Behaviors>
<UpdateTextSourceTriggerBehavior/>
</i:Interaction.Behaviors>
</TextBox>
This will keep the property on your viewmodel updated all the time, so that when you tap on save directly after typing in the textbox it will save the correct value. Hope it helps!
Cheers,
Anders