MAUI Community Toolkit EventToCommandBehavior - mvvm

I'm desperately trying to adhere to the MVVM design pattern in my App. So I'm trying to use the EventToCommandBehavior behavior from the MCT. (I'm also using the CommunityToolkit.Mvvm for [RelayCommand]) I've attached it to an Entry and am trying to forward the TextChanged event to my command. However, my command doesn't execute.
XAML:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
x:Class="MyApp.View.Accounts.AddAccountPage"
xmlns:viewmodel="clr-namespace:MyApp.ViewModel.AccountsViewModel"
Title="Add Account">
<VerticalStackLayout>
<Entry x:Name="entryAccountName"
Placeholder="Account Name"
PlaceholderColor="Black"
TextColor="{StaticResource Tertiary}"
BackgroundColor="{StaticResource Primary}"
WidthRequest="125"
HorizontalOptions="Center"
Keyboard="Text"
ClearButtonVisibility="WhileEditing"
ReturnType="Next">
<Entry.Behaviors>
<toolkit:EventToCommandBehavior
EventName="TextChanged"
Command="{Binding AccountTextChangedCommand}" />
</Entry.Behaviors>
</Entry>
....More XAML....
AccountsViewModel code:
[RelayCommand]
public void AccountTextChanged()
{
Application.Current.MainPage.DisplayAlert("Text changed", "Account Text Changed", "OK");
}
I've got a breakpoint set to the Method and it just never gets called. Any ideas as to what am I doing wrong?

If by "reference in the header", you mean the line xmlns:viewmodel=...,
all that can do is declare a namespace that can be used in the following xaml. It can't refer to a class in that namespace - the line you show is ignored because it is not a valid namespace - you need:
<ContentPage
xmlns:viewmodel="clr-namespace:MyApp.ViewModel" <--- NAMESPACE, NOT CLASS!
/>
<ContentPage.BindingContext>
<viewmodel:AccountsViewModel/>
</ContentPage.BindingContext>
lines in your xaml, to say which viewmodel is the binding context. This is equivalent to the c# code you put in constructor.
OR There is an alternative technique, using Dependency Injection.
In MauiProgram.CreateMauiApp(), add lines similar to:
mauiAppBuilder.Services.AddTransient<MyApp.ViewModel.AccountsViewModel>();
mauiAppBuilder.Services.AddTransient<MyApp.View.Accounts.AddAccountPage>();
Exact details of those lines depend on your existing CreateMauiApp code, and your namespaces.

I finally figured it out. I had forgotten to set the BindingContext to the AccountsViewModel in the constructor of the page:
using MyApp.Services;
using MyApp.ViewModel.AccountsViewModel;
namespace MyApp.View.Accounts;
public partial class AddAccountPage : ContentPage
{
public AddAccountPage()
{
InitializeComponent();
DataService dataService = new();
AccountsViewModel accountsViewModel = new(dataService);
BindingContext = accountsViewModel;
}
}
I've been having to set the BindingContext via constructors on various pages to get the data bindings to work, even though I reference the ViewModels in the XAML header. Is this intended or am I going about it wrong?

Related

how to reach components in macrocomponent by id?

I have a zul file (MainPage.zul) which contains a macrocomponent (configtabs). Macrocomponent's zul file inturn contains another macrocomponent(fieldListBox). How can I use the id of second macrocomponent(fieldListBox) in my MainPage's Controller class? I want to set model for second macrocomponent in doAfterCompose method of MainPage's cOntroller class.
Example code:
<?component name="configtabs" macro-uri="iam.configtab.zul" ?>
<zk>
<window>
<configtabs />
</window>
</zk>
configtab.zul
<hbox>
<fieldListBox id="fieldsbox" />
</hbox>
You can use zk selectors for that.
Click here for a little bit of explanation of what are those selectors.
Also every component has query methods. If you use those methods with the selectors you can query for components inside other components. It's been very useful to me.
For example on your doAfterCompose you can do:
configtabs.queryAll("fieldListBox")
Or
configtabs.queryAll("#fieldsbox")
And it returns the component or components that you want to set model.
I hope it's helpful. It depends on the context.

Tapestry 5 custom component in form - access during validation

A have problem with accessing my custom components (which are used as parts of the form).
Here is the story:
I have dynamic form which have few modes of work. Each mode can be selected and loaded into form body with AJAX. It looks like that (template):
<t:form t:id = "form">
<p class= "calcModeTitle">
${message:modeLabel}: <select t:id="modeSelect"
t:type="select"
t:model="modesModel"
t:value="selectedMode"
t:blankOption="NEVER"
t:encoder="modeEncoder"
t:zone = "modeZone"
/>
</p>
<div class="horizontal_tab">
<t:errors/>
</div>
<t:zone t:id="modeZone" id="modeZone" t:update="show">
<t:if test="showCompany">
<t:delegate to="block:companyBlock" />
</t:if>
<t:if test="showPersonal">
<t:delegate to="block:personalBlock" />
</t:if>
<t:if test="showMulti">
<t:delegate to="block:multiBlock" />
</t:if>
</t:zone>
<t:block id="companyBlock">
<t:modes.CompanyMode t:id="company"/>
</t:block>
<t:block id="personalBlock">
<t:modes.PersonalMode t:id="personal" />
</t:block>
<t:block id="multiBlock">
<t:modes.MultiMode t:id="multi" />
</t:block>
<div class="horizontal_tab">
<input type="submit" value="${message:submit_label}" class="submitButton thickBtn"/>
</div>
</t:form>
AJAX works pretty well and form changes accordingly the "modeSelect" state. But i run into problem when submitting the form. I have in class definition hooks for components placed as:
//----form elements
#Component(id = "form")
private Form form;
#InjectComponent
private CompanyMode company;
#InjectComponent
private PersonalMode personal;
#InjectComponent
private MultiMode multi;
where *Mode classes are my own components, containing form elements and input components. I planned to get access to them during validation, and check values supplied by user with form, but when I am trying to reach anything from them I've got nullPointerException - it seems that component are not initialized in my class definition of form. On the other hand form component is injected properly (I am able to write some error for example). I am a bit lost now. How to properly inject my components to class page containing the form?
Dynamic forms in tapestry are a bit complicated. Tapestry passes a t:formdata request parameter which contains the serialized form entities. This is then used serverside in the POST to re-hydrate initial form state. This must be kept up-to-date with what the client sees.
If you want to add dynamic content to a form via ajax, you will need to use the FormInjector. You might want to take a look at the source code for the AjaxFormLoop to see an example.
If you want to render hidden form fragments and make them visible based on clientside logic, you can use the FormFragment
From tapestry guide:
A block does not normally render; any component or contents you put
inside a block will not ordinarily be rendered. However, by injecting
the block you have precise control over when and if the content
renders.
Try to use here either "t:if" or "t:delegate".
Something like this:
<t:zone t:id="modeZone" id="modeZone" t:update="show">
<t:delegate to="myBlock" />
</t:zone>
<t:block t:id="companyBlock">
<t:modes.CompanyMode t:id="company"/>
</t:block>
<t:block t:id="personalBlock">
<t:modes.PersonalMode t:id="personal" />
</t:block>
<t:block t:id="multiBlock">
<t:modes.MultiMode t:id="multi" />
</t:block>
java:
#Inject
private Block companyBlock, personalBlock, multiBlock;
public Block getMyBlock(){
if (getShowCompany()) return companyBlock;
if (getShowPersonal()) return personalBlock;
return multiBlock;
}

Struts 2 interceptor redirect JSP page

Using Struts 2, I have this interceptor:
public class AccessInterceptor extends AbstractInterceptor {
#Override
public String intercept(ActionInvocation ai) throws Exception {
ActionContext actionContext = ai.getInvocationContext();
Map<String, Object> session = actionContext.getSession();
Boolean logged = (Boolean) session.get("logged");
if(logged == null) {
session.put("logged", false);
logged = false;
}
if (!logged) {
// If exception is here, it IS thrown, so I am sure "login" is returned on /homepage
return "login";
}
return ai.invoke();
}
}
And this struts.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<constant name="struts.devMode" value="true" />
<package name="loginInterceptor" namespace="/" extends="struts-default">
<interceptors>
<interceptor name="access"
class="logininterceptor.interceptors.AccessInterceptor"/>
<interceptor-stack name="appDefault">
<interceptor-ref name="access" />
<interceptor-ref name="defaultStack" />
</interceptor-stack>
</interceptors>
<default-interceptor-ref name="appDefault" />
<global-results>
<result name="login">/WEB-INF/content/login.jsp</result>
</global-results>
</package>
</struts>
I have also /WEB-INF/content/homepage.jsp JSP and /WEB-INF/content/login.jsp and mypackage.LoginAction.java class. There is not an acton class for homepage
The problem is, that when I go to the /homepage page, it is NOT redirected to login.jsp and I see the homepage.jsp content. What is wrong?
PS: I use struts2-convention-plugin
This is happening because the conventions plugin uses the "convention-default" package which does not know about your interceptor or interceptor stack.
If you could create a package which extends conventions-default doing just what your interceptor stack does (defines the interceptor, interceptor stack and defines the default result type) and then make the conventions plugin apply that stack as the default for all actions wouldn't that be great? You can.
After the <struts> tag define a constant like so:
<constant name="struts.convention.default.parent.package" value="loginInterceptor"/>
The other suggestion, was to use struts.xml to override the conventions, which is fine but a little verbose.
Another option is to use the parent package annotation, which is good when you need to override the default behavior but as you pointed out you want this to be the default behavior so you should reserve annotations for overriding this, not to implement it.
It may be worth browsing the following list of struts2 conventions plugin constants for the future: http://struts.apache.org/2.1.8/docs/convention-plugin.html#ConventionPlugin-Overwritingpluginclasses
PS: Just naming preference but I would call your package login-package rather than loginInterceptor... not a big deal.
If you want to force to execute the interceptor every time anyone is accessing to homepage.jsp, the correct way of doing this is to put an action that returns the jsp. For example:
<struts>
...
<package name="loginInterceptor" namespace="/" extends="struts-default">
<interceptors>
...
</interceptors>
<default-interceptor-ref name="appDefault" />
<global-results>
...
</global-results>
<action name="homepage">
<result>/WEB-INF/content/homepage.jsp</result>
</action>
</package>
</struts>
After that, when a user tries to access to http://.../homepage.action, the interceptor will be executed and will be redirected to the login page if the user is not logged in.

Setting Window.Content to ViewModel - simple data template not working

Trying to get my head around MVVM, and getting a simple window to render its viewmodel as a view via data templating.
in App.xaml:
<Application.Resources>
<DataTemplate DataType="{x:Type vm:TestViewModel}">
<vw:TestView />
</DataTemplate>
</Application.Resources>
View definition:
<UserControl x:Class="MyNamespace.TestView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<TextBlock>TESTING</TextBlock>
</Grid>
</UserControl>
In MainWindowViewModel:
private void onOpenTestView(object sender)
{
Window w = new Window();
w.Content = new TestViewModel();
w.Show();
}
Running the app results in a window with a "MyNamespace.TestViewModel" string, instead of "TESTING", which would infer my data template is not being found.
I am very new to all this so am I missing something obvious? I don't think it's a string matching issue since if I deliberately misspell the view or model in the XAML then it doesn't compile.
Should my new window be able to access my app resources (and thus my datatemplate) ok?
Cheers,
Jeremy
EDIT: FIXED (Can't answer for 8 hours)
Due to MS Bug where resources not being read unless at least one style is set.
See http://connect.microsoft.com/VisualStudio/feedback/details/553528/defaultstylekey-style-not-found-in-inner-mergeddictionaries
Due to MS Bug where resources not being read unless at least one style is set.
See http://connect.microsoft.com/VisualStudio/feedback/details/553528/defaultstylekey-style-not-found-in-inner-mergeddictionaries

Custom forms in Magento

Can anyone provide a dummy guide \ code snippets on how to create a front end form in Magento that posts data to a controller action.
Im trying to write a variant of the contact us from. (I know its easy to modify the contact us form, as outlined here). I'm trying to also create a feedback form with additional fields.
Given this basic form:
<form action="<?php echo $this->getFormAction(); ?>" id="feedbackForm" method="post">
<div class="input-box">
<label for="name"><?php echo Mage::helper('contacts')->__('Name') ?> <span class="required">*</span></label><br />
<input name="name" id="name" title="<?php echo Mage::helper('contacts')->__('Name') ?>" value="<?php echo $this->htmlEscape($this->helper('contacts')->getUserName()) ?>" class="required-entry input-text" type="text" />
</div>
<div class="button-set">
<p class="required"><?php echo Mage::helper('contacts')->__('* Required Fields') ?></p>
<button class="form-button" type="submit"><span><?php echo Mage::helper('contacts')->__('Submit') ?></span></button>
</div>
</form>
What are the basic step I need to take to get inputted name to a controller action for processing?
If any one is interested, I solved this by building my own module which was heavily based on the Magento_Contacts module.
Here are some links that helped me figure things out.
http://www.magentocommerce.com/wiki/custom_module_with_custom_database_table
http://inchoo.net/ecommerce/magento/magento-custom-emails/
To make $this->getFormAction() return the URL to your custom controller, you have two options:
call setFormAction() somewhere else on the block.
use a custom block type that implements getFormAction().
(1) is what happens in Mage_Contacts_IndexController::indexAction(), but (2) is the cleaner approach and I'm going to explain it in detail:
Create a custom module
app/etc/modules/Stack_Form.xml:
<?xml version="1.0"?>
<config>
<modules>
<Stack_Form>
<active>true</active>
<codePool>local</codePool>
</Stack_Form>
</modules>
</config>
app/code/local/Stack/Form/etc/config.xml:
<?xml version="1.0"?>
<config>
<modules>
<Stack_Form>
<version>0.1.0</version>
</Stack_Form>
</modules>
<frontend>
<routers>
<stack_form>
<use>standard</use>
<args>
<module>Stack_Form</module>
<frontName>feedback</frontName>
</args>
</stack_form>
</routers>
</frontend>
<global>
<blocks>
<stack_form>
<class>Stack_Form_Block</class>
</stack_form>
</blocks>
</global>
</config>
This configuration registers the stack_form block alias for own blocks and the feedback front name for own controllers.
Create custom block
app/code/local/Stack/Form/Block/Form.php
class Stack_Form_Block_Form extends Mage_Core_Block_Template
{
public function getFormAction()
{
return $this->getUrl('stack_form/index/post`);
}
}
Here we implemented getFormAction() to generate the URL for our custom controller (the result will be BASE_URL/feedback/index/post).
Create custom controller
app/code/local/Stack/Form/controllers/IndexController.php
class Stack_Form_IndexController extends Mage_Contacts_IndexController
{
public function postAction()
{
// your custom post action
}
}
If the form should behave exactly like the contact form, just with a different email template and additional form fields, there are two solutions that I have outlined at https://magento.stackexchange.com/q/79602/243 where only one of them actually requires a custom controller action to send the form:
If you look at the contacts
controller
used in the form action, you will find that
the transactional template is taken directly from the configuration
all POST data is passed to the template (as template variable data), so that you can add any additional fields to the form
template and use them in the email template. But validation is hard
coded for "name", "comment", "email" and "hideit".
So, if you need a completely different email template or
additional/changed input validation, your best bet is to create a
custom controller with a modified copy of the postAction of
Mage_Contacts_IndexController.
But there is another solution that is a bit limited but without any
custom code involved:
create a hidden input that determines the type of the form. It could be just <input type="hidden" name="custom" value="1" />.
in the contact transactional email template, use the if directive to show different content based on the form type:
{{if data.custom}}
... custom contact form email ...
{{else}}
... standard contact form email ...
{{/if}}
How to use this custom block
You can add the form anywhere in the CMS using this code (CMS directive):
{{block type="stack_form/form" template="path/to/your/form.phtml"}}
If you do this, you need to add "stack_form/form" to the block whitelist under System > Permissions > Blocks!
Or in the layout using this code (layout XML):
<block type="stack_form/form" name="any_unique_name" template="path/to/your/form.phtml" />
Solution without custom module
If you use the solution without custom controller and a single email template mentioned above, you can set the form action using layout XML as well.
To achieve this, we use the feature to call helpers as parameters for block actions. Unfortunately, the core helper does not have a public method to get a URL but the helper from Mage_XmlConnect has, so you can use that one:
<block type="core/template" name="any_unique_name" template="path/to/your/form.phtml">
<action method="setFormAction">
<param helper="xmlconnect/getUrl">
<route>contacts/index/post</route>
</param>
</action
</block>
In the CMS directive you cannot use helpers, so there you would need to put the actual URL:
{{block type="stack_form/form" template="path/to/your/form.phtml" form_action="/feedback/index/post"}}
Since you probably have different CMS pages/blocks in different store views, this should not be a big problem.