How to display popup in a template and pass it arguments - mvvm

Part of my application is recording the finish times of a race. Since this will most likely be done on a phone or tablet I would like to implement a small popup to easily modify the time without having to set the focus exactly and type it in. However having the time start as 00:00:00 for every finish time will make the process very laborious so I want to have it initialise to the last entered finish time. I want the popup to appear directly below the timebox, if times being entered are at the top of the grid, or above the timebox for times being entered which are at the bottom of the grid. Below is stripped down versions of my code which hopefully helps explain the concept.
My popup window: entertime.zul
<window viewModel="#id('vmtp') #init('EnterTimeVM')" onBlur="#command('close')">
<caption>
<toolbarbutton label="Save" onClick="#command('save')"/>
<toolbarbutton label="Cancel" onClick="#command('close')"/>
</caption>
<hlayout>
<vlayout>
<button label="+" onClick="#command('changeHours', amount='1')" />
<intbox value="#load(vmtp.hours)" readonly="true" />
<button label="-" onClick="#command('changeHours', amount='-1')" />
</vlayout>
<vlayout>
<button label="+" onClick="#command('changeMinutes', amount='1')" />
<intbox value="#load(vmtp.minutes)" readonly="true" />
<button label="-" onClick="#command('changeMinutes', amount='-1')" />
</vlayout>
<vlayout>
<button label="+" onClick="#command('changeSeconds', amount='1')" />
<intbox value="#load(vmtp.seconds)" readonly="true" />
<button label="-" onClick="#command('changeSeconds', amount='-1')" />
</vlayout>
</hlayout>
</window>
EnterTimeVM.java
public class EnterTimeVM {
private LocalDateTime ldt;
private Component view;
#Init
public void init(#ExecutionArgParam("initTime") LocalDateTime initTime,
#ContextParam(ContextType.VIEW) Component view) {
ldt = initTime;
this.view = view;
}
public int getHours() {
return ldt.getHour();
}
public int getMinutes() {
return ldt.getMinute();
}
public int getSeconds() {
return ldt.getSecond();
}
#Command
#NotifyChange("hours")
public void changeHours(#BindingParam("amount") int amount) {
ldt = ldt.plusHours(amount);
}
#Command
#NotifyChange({ "hours", "minutes" })
public void changeMinutes(#BindingParam("amount") int amount) {
ldt = ldt.plusMinutes(amount);
}
#Command
#NotifyChange({ "hours", "minutes", "seconds" })
public void changeSeconds(#BindingParam("amount") int amount) {
ldt = ldt.plusSeconds(amount);
}
#Command
public void save() {
Map<String, Object> args = new HashMap<>();
args.put("finishTime", ldt);
BindUtils.postGlobalCommand(null, null, "finishTime", args);
close();
}
#Command
public void close() {
view.detach();
}
}
Here is my main zul and view model.
timekeeper.zul (excess columns removed for brevity)
<window viewModel="#id('vmtk') #init('TimeKeeperVM')">
<grid model="#load(vmtk.competitors)">
<columns>
<column label="Name" />
<column label="Finish time" />
</columns>
<template name="model">
<row>
<label value="#load(each.name)" />
<timebox format="HH:mm:ss" value="#bind(each.finishTime)"
onFocus="#command('changeFinishTime', comp=each)" />
</row>
</template>
</grid>
</window>
Competitor.java
public class Competitor {
private String name;
private LocalDateTime finishTime;
// getters and setters
}
TimeKeeperVM.java
public class TimeKeeperVM {
private List<Competitor> competitors;
private Competitor selectedCompetitor;
private LocalDateTime prevFinishTime;
#Init
public void timeKeeperInit() {
prevInitTime = LocalDateTime.now();
}
public List<Competitor> getCompetitors() {
return competitors;
}
#Command
public void changeFinishTime(#BindingParam("comp") Competitor competitor,
#ContextParam(ContextType.COMPONENT) Component timebox) {
selectedCompetitor = competitor;
Map<String, Object> args = new HashMap<>();
LocalDateTime currentFinishTime = competitor.getFinishTime();
args.put("initTime", (currentFinishTime != null) ? currentFinishTime : prevFinishTime);
Window win = (Window) Executions.createComponents("entertime.zul", timebox.getParent(), args);
// Need to use the parent of timebox in this case
win.setPosition("parent,bottom,right"); // positions the popup relative to timebox parent, not timebox
win.doPopup();
}
#GlobalCommand
#NotifyChange("competitors")
public void finishTime(#BindingParam("finishTime") LocalDateTime finishTime) {
if (selectedCompetitor != null && finishTime != null) {
selectedCompetitor.setFinishTime(finishTime);
prevFinishTime = finishTime;
}
}
}
The code as I have it at the moment (i.e programatically create the popup - see changeFinishTime method) displays the popup but not in the ideal position. As per the zk popup demo I could generate the popup in the zul by having somewhere in the zul file:
<popup id="timepop">
<include src="entertime.zul" />
</popup>
and then displaying it by:
onFocus='timepop.open(self,#load(vm.popupPosition))'
The problem with this is that I can't pass args to entertime.zul. Also I can't modify the position of the popup as popupPosition will be resolved at render time; not runtime. This is the same problem if the include line (from above) is changed to:
<include initTime="#load(vm.prevFinishTime)" src="entertime.zul" />
initTime is initialised at render time; not runtime.
Any thoughts/advice greatly appreciated.

I would prefer to use the Executions.createComponents solution.
If the position of the modal win is the same for all the windows, I usually tag directly the position in window component:
<window viewModel="#id('vmtp') #init('EnterTimeVM')" onBlur="#command('close')" position="parent, bottom, right" width="100px">
instead of set it VM.
Then, did you try to remove the position? In my testing project with your code the popup is opened next the timebox.getParent().
With your code, the timebox.getParent is the component Row, so maybe there can be problems with row width, for example.
You can bypass the problem use a parent component before timebox like hbox.
<hbox>
<timebox format="HH:mm:ss" value="#bind(each.finishTime)" onFocus="#command('changeFinishTime', comp=each)" />
</hbox>
so that the parent result a little more usable.

I was hoping to position the popup relative to the row the popup is attached to. I didn't read the api of Window's setPosition properly. It says Position the window relative to its parent. That is, the left and top is an offset to his parent's left-top corner. But I can manipulate the position using session attributes:
#Command
public void changeFinishTime(#BindingParam("comp") Competitor competitor,
#ContextParam(ContextType.COMPONENT) Component timebox) {
selectedCompetitor = competitor;
// set args map
Window win = (Window) Executions.createComponents("entertime.zul", timebox.getParent(), args);
Sessions.getCurrent().setAttribute("top", "-20px");
win.doPopup();
}
And then change entertime.zul:
<window viewModel="#id('vmtp') #init('EnterTimeVM')" onBlur="#command('close')" position="parent" top="${sessionScope.top}" width="100px">
This solution is a little clunky and will have to look into how much of an issue it is if the font size changes but it does achieve what I want.
I could also remove all the positioning from the entertime.zul window element and do it in java:
Window win = (Window) Executions.createComponents("entertime.zul", timebox.getParent(), args);
win.setPosition("parent");
win.setTop("-20px");
win.doPopup();

Related

Custom control - event handler to main app when inside value changed

I want to be able to add an event from my custom control to the outside world
as I change a value in my custom control, I use TwoWay, but I also want an event to be generated in my main app
I just cannot find anything relevant to this
the main app xaml contains this line :
<custom:MyComp x:Name="myComp" ValueChanged="SomeFunction" Value="{ Binding Path=someIntVariable, Mode=TwoWay}"></custom:MyComp>
how do I implement a ValueChanged event in the custom control ?
my control xaml:
<UserControl ...>
<StackPanel Orientation="Horizontal" >
...
<TextBox x:Name="MyCompTextBox" Width="50" Height="20" TextChanged="MyCompTextBox_TextChanged"/>
...
</StackPanel>
</UserControl>
the code
public partial class MyComp: UserControl
{
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(int), typeof(MyComp), new FrameworkPropertyMetadata(0, OnValuePropertyChangedCallback));
public int Value
{
get { return (int)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
private static void OnValuePropertyChangedCallback(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
MyComp mMyComp= source as MyComp;
int newValue=(int)e.NewValue;
myComp.MyCompTextBox.Text = newValue.ToString();
}
public MyComp()
{
InitializeComponent();
}
private void MyCompTextBox_TextChanged(object sender, EventArgs e)
{
int _bpm = int.ParseMyCompTextBox.Text);
Value = _bpm;
MyCompTextBox.Text = Value.ToString();
// here I want to trigger an even to the main app !!!
}
}
thanks for your help

Xamarin Layout can't receive focus

I'm trying to create a compound view component in Xamarin Forms called FormElement which is composed of two labels and an Entry:
<?xml version="1.0" encoding="UTF-8"?>
<StackLayout xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:custom="clr-namespace:Mynamespace;assembly=Mynamespace"
x:Class="Mynamespace.Components.FormEntry">
<StackLayout Orientation="Horizontal">
<Label x:Name="formRequiredStar"
IsVisible="{Binding IsRequired}"
Text="*" TextColor="Red"
FontSize="15"
FontAttributes="Bold"
Margin="-12,0,0,0"
HorizontalOptions="Start" />
<Label x:Name="formLabel"
HorizontalOptions="Start"
Text="{Binding LabelText}"
TextColor="{Binding LabelTextColor}"
FontSize="{Binding LabelTextFontSize}"
FontAttributes="{Binding LabelTextFontStyle}" />
</StackLayout>
<Frame BorderColor="Black"
CornerRadius="7"
Padding="5,0"
Margin="0,-3,0,0"
HasShadow="false">
<Entry x:Name="mainEntry"
Keyboard="{Binding KeybdType}"
Placeholder="{Binding EntryPlaceHolder}"
TextColor="Black"
FontSize="Default"
HeightRequest="{Binding EntryHeight}" />
</Frame>
</StackLayout>
Next, I want to switch focus from the Entry to a "next" element when the user taps the DONE button, so I do:
namespace Mynamespace.Components
{
public partial class FormEntry : StackLayout
{
public VisualElement NextFocus
{
get { return (VisualElement)GetValue(NextFocusProperty); }
set { SetValue(NextFocusProperty, value); }
}
public static readonly BindableProperty NextFocusProperty =
BindableProperty.Create(nameof(NextFocus),
typeof(VisualElement),
typeof(FormEntry),
null,
Xamarin.Forms.BindingMode.OneWay);
public FormEntry()
{
InitializeComponent();
BindingContext = this;
mainEntry.Completed += (s, e) =>
{
if (NextFocus != null)
{
NextFocus.Focus();
}
};
}
}
}
Next, in order for a FormEntry to be the target of NextFocus, I tried adding
this.Focused += (s,e) => { mainEntry.Focus(); };
to the constructor, but the handler is never called, and I also tried overriding
public new void Focus() {
mainEntry.Focus();
}
but this method is never called. Layout classes are descended from VisualElement so they should inherit Focused. Is there something about Layout objects that I'm missing? I could understand that Layout objects aren't usually the target of focus, but the event handler is supposedly there so I ought to be able to use it.
Here's an example of how I utilize the FormEntry on a login screen:
<!-- Email -->
<controls:FormEntry x:Name="usernameEntry"
Margin="25,40,25,0"
IsRequired="true"
EntryHeight="40"
KeybdType="Email"
NextFocus="{x:Reference passwordEntry}"
LabelText="{il8n:Translate Emailorusername}"
EntryPlaceHolder="{il8n:Translate EnterUsername}">
</controls:FormEntry>
<!-- Password -->
<controls:FormEntry x:Name="passwordEntry"
Margin="25,0,25,0"
IsRequired="true"
EntryHeight="40"
LabelText="{il8n:Translate Password}"
EntryPlaceHolder="{il8n:Translate EnterPassword}" />
I think you have get the nextfocus element, you can get mainEntry from nextfocus, like this:
public FormEntry ()
{
InitializeComponent ();
BindingContext = this;
mainEntry.Completed += (s, e) =>
{
if (NextFocus != null)
{
FormEntry formentry = (FormEntry)NextFocus;
Entry entry = formentry.mainEntry;
entry.Focus();
}
};
}
Then you can find you will get focus.

Insert text at caret from button in RichTextBox mvvm using wpf toolkit

I have selected text from a ListBox that I want to insert into a RichTextBox at the caret position. I can get the selected text to be inserted at the end of the text string.
I am not sure how to pass the RichTextBox caret position to my view model.
Here is part of my code for the project.
<Button x:Name="AddItemBtn" Content="Add Item" HorizontalAlignment="Left" Margin="417,10,0,0" VerticalAlignment="Top" Width="100" Command="{Binding AddItemBtn}" CommandParameter="{Binding ElementName=AddItemList,Path=SelectedItem}"/>
<wpftoolkit:RichTextBox Grid.Column="0" Text="{Binding TestText, UpdateSourceTrigger=PropertyChanged}" x:Name="MyEditor" ScrollViewer.VerticalScrollBarVisibility="Auto" Margin="0" Height="Auto" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Width="Auto" IsDocumentEnabled="True" AcceptsTab="True" AcceptsReturn="True" >
<wpftoolkit:RichTextBox.Resources>
<Style TargetType="{x:Type Paragraph}">
<Setter Property="Margin" Value="0" ></Setter>
<Setter Property="FontSize" Value="15"></Setter>
</Style>
</wpftoolkit:RichTextBox.Resources>
<wpftoolkit:RichTextBox.TextFormatter>
<wpftoolkit:PlainTextFormatter/>
</wpftoolkit:RichTextBox.TextFormatter>
</wpftoolkit:RichTextBox>
Here is the view model part.
private string _testText;
public string TestText
{
get
{
return _testText;
}
set
{
//_testText = _testText + value;
SetProperty(ref _testText, value);
}
}
public ICommand AddItemBtn
{
get;
set;
}
public void addItem(Tabbed selectedItem)
{
if (selectedItem != null)
{
MessageBox.Show(selectedItem.Command);
if (TestText != null)
{
TestText = TestText.ToString() + selectedItem.Command;
}
else
{
TestText = selectedItem.Command;
}
}
}
I tried a flowdocument but still could not get the parameters to pass correctly.
I like to put a function on the view model that is setup in the view code behind.
public class MainViewModel : ViewModelBase
{
public Func<int> GetCarrotPosition { get; set; }
//...
Looks like you can get the number of characters into the text string by getting the offset from the document start start to the current position
public MainWindow()
{
// InitializeComponent stuff..
var castedContext = (MainViewModel)DataContext;
castedContext.GetCarrotPosition = () =>
{
// Placing the cursor at the start of the text returns a value of 2, so I subtract 2 to get the current cursor location
return MyRichTextBox.CaretPosition.DocumentStart.GetOffsetToPosition(MyRichTextBox.CaretPosition) - 2;
};
//...
Finally, call the GetCarrotPosition() function in your command
var carrotPosition = GetCarrotPosition();
TestText.Insert(carrotPosition, selectedItem.Command);
Creating delegates on the view model that get wired up in the view code behind is the sexiest MVVM way of working with UI elements I know of.

Template 10 UWP How to bind to a autoSuggestBox inside a MenuFlyoutItem

I am using the MVVM pattern to bind the properties of the AutoSuggestBox in a ViewPage to my ViewModel. This works fine when I am inside a Grid or a stackPanel.
But once I put the AutoSuggestBox inside a MenuFlyout of a Button. I get the following Error at compile time
Error Object reference not set to an instance of an object.
Any guidance on how to bind the properties of AutoSuggestBox inside the MenuFlyoutItem??
Here is the code I am trying to compile.
<Button>
<Button.Flyout>
<MenuFlyoutItem >
<MenuFlyoutItem.Template>
<ControlTemplate TargetType="MenuFlyoutItem">
<AutoSuggestBox Header="What's your name?"
TextChanged="{x:Bind ViewModel.FilterUsuals}"
QuerySubmitted="{x:Bind ViewModel.ProcessQuery}"
SuggestionChosen="{x:Bind ViewModel.ProcessChoice}"
ItemsSource="{Binding Elements}"
Text="{x:Bind ViewModel.SearchText, Mode=TwoWay}"
QueryIcon="Find" />
</ControlTemplate>
</MenuFlyoutItem.Template>
</MenuFlyoutItem>
</Button.Flyout>
</Button >
<Button Content="Button" Margin="10,0" >
<Button.Flyout>
<Flyout Placement="Top">
<AutoSuggestBox ... />
</Flyout>
</Button.Flyout>
</Button>
Not sure as to the nature of the need for it to be in a MenuFlyout. Why cause yourself so much pain doing it that way when it can be just in a Flyout subtype within the button itself?
As for the binding this has nothing to do with Template10. It's probably related to a collection that wasn't initialized. Verify those collections you are binding to have been created correctly (i.e. new List<yourtype>() for example)
I believe your error is because you are using a ControlTemplate that immediately changes the data context from the page, making your ViewModel out of scope. More importantly is that x:Bind is not supported in ControlTemplates. This means you can't use the handy x:Bind to Events and will need to create commands. You will have to use behaviors to accomplish this most easily.
Something similar to this.
<AutoSuggestBox>
<interactivity:Interaction.Behaviors>
<core:EventTriggerBehavior EventName="TextChanged">
<core:InvokeCommandAction Command="{Binding TextChangedCommand}" />
</core:EventTriggerBehavior>
</interactivity:Interaction.Behaviors>
</AutoSuggestBox>
Or similar to this.
public class AutoSuggestBoxAttachedProperties : Windows.UI.Xaml.DependencyObject
{
public static ICommand GetTextChangedCommand(Windows.UI.Xaml.Controls.AutoSuggestBox obj)
=> (ICommand)obj.GetValue(TextChangedCommandProperty);
public static void SetTextChangedCommand(Windows.UI.Xaml.Controls.AutoSuggestBox obj, ICommand value)
=> obj.SetValue(TextChangedCommandProperty, value);
public static readonly DependencyProperty TextChangedCommandProperty =
DependencyProperty.RegisterAttached("TextChangedCommand", typeof(ICommand),
typeof(AutoSuggestBoxAttachedProperties), new PropertyMetadata(null, TextChangedCommandChanged));
public static object GetTextChangedCommandParameter(Windows.UI.Xaml.Controls.AutoSuggestBox obj)
=> (object)obj.GetValue(TextChangedCommandParameterProperty);
public static void SetTextChangedCommandParameter(Windows.UI.Xaml.Controls.AutoSuggestBox obj, object value)
=> obj.SetValue(TextChangedCommandParameterProperty, value);
public static readonly DependencyProperty TextChangedCommandParameterProperty =
DependencyProperty.RegisterAttached("TextChangedCommandParameter", typeof(object),
typeof(AutoSuggestBoxAttachedProperties), new PropertyMetadata(null));
private static void TextChangedCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var box = d as Windows.UI.Xaml.Controls.AutoSuggestBox;
box.TextChanged -= Box_TextChanged;
if (e.NewValue != null)
{
box.TextChanged += Box_TextChanged;
}
}
private static void Box_TextChanged(Windows.UI.Xaml.Controls.AutoSuggestBox sender, Windows.UI.Xaml.Controls.AutoSuggestBoxTextChangedEventArgs args)
{
var command = GetTextChangedCommand(sender);
if (command != null)
{
var parameter = GetTextChangedCommandParameter(sender);
command.Execute(parameter);
}
}
}
Then this.
<AutoSuggestBox
ex:AutoSuggestBoxAttachedProperties.TextChangedCommand="{Binding TextChangedCommand}" />
Best of luck. /Jerry

From an select item in the list, create another listbox ZK

I had a headache with this. I want to choose a book from the 1st list and with that book create a second list to be able to show the details of the book (title, number of pages)
Here is the code:
public class Book {
private int numBook;
private String nameBook;
private String author;
public Book(int numBook, String nameBook, String author) {
super();
this.numBook = numBook;
this.nameBook = nameBook;
this.author = author;
}
public int getNumBook() {
return numBook;
}
public void setNumBook(int numBook) {
this.numBook = numBook;
}
public String getNameBook() {
return nameBook;
}
public void setNameBook(String nameBook) {
this.nameBook = nameBook;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
Class BookData: Load the info in array
public class BookData {
private List<Book> books = new ArrayList<Book>();
public BookData() {
loadBooks();
}
public List<Book> getBooks() {
return books;
}
public void setBooks(List<Book> books) {
this.books = books;
}
public void loadBooks() {
Book b;
for(int i = 0; i<4;i++){
b = new Book(i+1, "Libro "+i+1, "Author "+i+1);
books.add(b);
}
}
}
Class BookViewModel: ViewModel of Listbox
public class BookViewModel {
private static Book selectedBook;
private List<Book> booksData = new ArrayList<Book>(new BookData().getBooks()); // Armo los libros
public List<Book> getBooksData() {
return booksData;
}
public void setBooksData(List<Book> booksData) {
this.booksData = booksData;
}
//Getters and Setter the SelectedCar
#NotifyChange("selectedBook")
public Book getSelectedBook() {
if(selectedBook!=null) {
//setSelectedBook(selectedBook);
new DetailData(selectedBook);
//new ArrayList<>(new DetailData().getDetailsFilterByBook());
//Then here pass the Book Selected
}
return selectedBook;
}
public void setSelectedBook(Book selectedBook) {
this.selectedBook = selectedBook;
}
}
Class Detail: Detail Model of the choose Book
public class Detail {
private int idBook;
private String title;
private int numPages;
public Detail(int idBook, String title, int numPages) {
this.idBook = idBook;
this.title = title;
this.numPages = numPages;
}
public int getIdBook() {
return idBook;
}
public void setIdBook(int idBook) {
this.idBook = idBook;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public int getNumPages() {
return numPages;
}
public void setNumPages(int numPages) {
this.numPages = numPages;
}
#Override
public String toString() {
return "Detail [idBook=" + idBook + ", title=" + title + ", numPages=" + numPages + "]";
}
}
Class DetailData: Load the data in array
//Clase que se ecarga de manejar la data
public class DetailData {
private List<Detail> details = loadAllDetails();
private List<Detail> detailsFilterByBook;
private static Book bookSelected;
/*public DetailData(){
//Previously all the data is loaded
System.out.println(bookSelected);
detailsFilterByBook = new ArrayList<>();
filterDetailsByBook();
}*/
public void setBookSelected(Book bookSelected){
this.bookSelected = bookSelected;
}
public DetailData(){
this(bookSelected);
}
public DetailData(Book b){
bookSelected = b;
System.out.println(bookSelected);
detailsFilterByBook = new ArrayList<>();
filterDetailsByBook();
}
public List<Detail> loadAllDetails(){
List tmp = new ArrayList<Detail>();
//Libro 1
Detail d1b1 = new Detail(1, "Preview", 15);
Detail d2b1 = new Detail(1, "Inicio", 10);
Detail d3b1 = new Detail(1, "Zk Bind", 50);
//Libro 2
Detail d1b2 = new Detail(2, "Introduccion", 15);
Detail d2b2 = new Detail(2, "JAVA", 100);
Detail d3b2 = new Detail(2, "CSS", 25);
//Libro 3
Detail d1b3 = new Detail(3, "HTML", 35);
Detail d2b3 = new Detail(3, "Javascript", 40);
Detail d3b3 = new Detail(3, "Ajax", 25);
//Libro 4
Detail d1b4 = new Detail(4, "Android", 100);
Detail d2b4 = new Detail(4, "IOS", 100);
tmp.add(d1b1);
tmp.add(d2b1);
tmp.add(d3b1);
tmp.add(d1b2);
tmp.add(d2b2);
tmp.add(d3b2);
tmp.add(d1b3);
tmp.add(d2b3);
tmp.add(d3b3);
tmp.add(d1b4);
tmp.add(d2b4);
return tmp;
}
private void filterDetailsByBook() {
for(Detail d:details){
if(d.getIdBook() == bookSelected.getNumBook())
detailsFilterByBook.add(d);
}
print();
}
public void print(){
System.out.println("Imprimiendo detalles del libro escogido");
for(Detail d: detailsFilterByBook){
System.out.println(d);
}
}
public List<Detail> getDetails() {
return details;
}
public void setDetails(List<Detail> details) {
this.details = details;
}
public List<Detail> getDetailsFilterByBook() {
return detailsFilterByBook;
}
public void setDetailsFilterByBook(List<Detail> detailsFilterByBook) {
this.detailsFilterByBook = detailsFilterByBook;
}
}
Class: DetailViewModel:ViewModel of the second ListBox
public class DetailViewModel {
private List<Detail> detailsData = new ArrayList<>();
#NotifyChange("detailsData")
public void refreshList(){
System.out.println("REFRESH");
detailsData = new ArrayList<>(new DetailData().getDetailsFilterByBook());
}
public List<Detail> getDetailsData() {
return detailsData;
}
#NotifyChange("detailsData")
public void setDetailsData(List<Detail> detailsData) {
this.detailsData = detailsData;
}
}
Here is the zul file
<window title="" border="none" height="100%" apply="org.zkoss.bind.BindComposer" viewmodel="#id('vm') #init('book.BookViewModel')">
<listbox model="#bind(vm.booksData)" selecteditem="#bind(vm.selectedBook)" emptymessage="No car found in the result">
<listhead>
<listheader label="Num Libro"/>
<listheader label="Libro"/>
<listheader label="Autor"/>
</listhead>
<template name="model" var="book">
<listitem>
<listcell label="#bind(book.numBook)"/>
<listcell label="#bind(book.nameBook)"/>
<listcell label="#bind(book.author)"/>
</listitem>
</template>
</listbox>
<separator height="100px"/>
<window title="" border="none" height="100%" apply="org.zkoss.bind.BindComposer"
viewModel="#id('vm') #init('detail.DetailViewModel')">
<listbox model="#bind(vm.detailsData)" emptyMessage="No existen datos que presentar">
<listhead>
<listheader label="Num Capitulos"/>
<listheader label="Titulo del Cap"/>
</listhead>
<template name="model" var="detail">
<listitem>
<listcell label="#bind(detail.idBook)"/>
<listcell label="#bind(detail.title)"/>
<listcell label="#bind(detail.numPages)"/>
</listitem>
</template>
</listbox>
</window>
</window>
I try in the second listbox (At begin have to be empty), show the details of the book everytime when a book in the 1st listbox is selected. I get the correct info. When I choose a book, I get the correct details of that book, but my second listbox does'nt show anything. I will apreciate all the help. PD: Sorry for the english
Oke, there are more points to say on this code then you imagine.
Never use static for a user/session variable.
In your VM you have the following code :
private static Book selectedBook;
Imagine that I select Book 1 and you select 2 seconds later Book 2.
Because it's static, I'm also having Book 2 selected, while mine view isn't aware of it.
This means the GUI and server side are out of sync => never a good thing.
If you could be able to sync the view with the selected item, this means that you select book 2 for me and I'll be searching the number of the Ghost Busters.
With ZK, always use ListModel interface to give collections to GUI.
While returning List<Book> works pretty good, you need to understand the consequences of this action.
A List or Grid expect an implementation of ListModel and if you don't give it, there will be one created every time you notify the list of a change.
While this is a nice to have feature it also removes the intelligence of a listmodel and the GUI rendering will be a lot more.
An example is always more clear :
We have a Collection of 9 items and we will append 1 to it.
Adding 1 Object to the List and notifying it implies that all the content rendered of the Listbox will be removed and then adding all the content again to the Listbox.
This means that we are removing and adding 9 lines who aren't changed.
Adding 1 Object to a ListModel, even without notifying the ListModel of a change will result in an action where there is only 1 item appended to the Listbox. This is the intelligence of a ListModel => adding and removing items will be persisted to the GUI without overhead.
So your code should be looking like this :
private Book selectedBook;
private final ListModelList<Book> booksData = new ListModelList<Book>(new BookData().getBooks()); // Armo los libros
Why not working to the interface and why final?
So I just told you about the interface ListModel and yet, I'm setting an implementation of ListModel as code, even while we learn to work against interfaces.
The simple reason is that ListModel doesn't have methods for appending and removing items while the implementation do have it.
So I make a decision to work against that object in stead of casting it when I need the methods.
Remember, the global getter for the booksData can look like this :
public ListModel<Book> getBooksData() {
return booksData;
}
So here we hide the implementation of ListModelList to the outside.
The reason for final is that you will forcing yourself or other people who are going through the code to use the clear() method in stead of making a new ListModelList.
It's just not needed to create a new instance of it.
Using 2 viewmodel's
Your making yourself difficult of using 2 VM's.
But while it's sometimes a good idea to do this I'll be helping you to get your problem solved.
Your first problem is one of a naming kind.
Viewmodel 1 => called vm in the zul.
Viewmodel 2 => called vm in the zul.
You see it coming? who will listen when I cry to vm?
let's call the viewmodel of the details detailVM
viewModel="#id('detailVM') #init('detail.DetailViewModel')"
The second problem is that your detail viewmodel doesn't have any clue of the first listbox.
What do I want to say is that your second viewmodel should be holding the correct info of the selected item of the first listbox.
Zul code should be looking like this :
<window title="" border="none" height="100%" apply="org.zkoss.bind.BindComposer" viewmodel="#id('vm') #init('book.BookViewModel')">
<div apply="org.zkoss.bind.BindComposer"
viewModel="#id('detailVM') #init('detail.DetailViewModel')">
<listbox model="#init(vm.booksData)" selecteditem="#bind(detailVM.selectedBook)" emptymessage="No book found in the result">
<listhead>
<listheader label="Num Libro"/>
<listheader label="Libro"/>
<listheader label="Autor"/>
</listhead>
<template name="model" var="book">
<listitem>
<listcell label="#load(book.numBook)"/>
<listcell label="#load(book.nameBook)"/>
<listcell label="#load(book.author)"/>
</listitem>
</template>
</listbox>
<separator height="100px"/>
<listbox model="#init(detailVM.detailsData)" emptyMessage="No existen datos que presentar">
<listhead>
<listheader label="Num Capitulos"/>
<listheader label="Titulo del Cap"/>
</listhead>
<template name="model" var="detail">
<listitem>
<listcell label="#load(detail.idBook)"/>
<listcell label="#load(detail.title)"/>
<listcell label="#load(detail.numPages)"/>
</listitem>
</template>
</listbox>
</div>
</window>
So I set you up with the correct zul, and now it's up to you to modify the viewmodels.
Remember that I set selectedBook in detailVM so now it's not needed in the first viewmodel.
I don't write everything for you, otherwise you wouldn't learn from it.
Some small things left to say.
You see I change the listbox model to #init and not #bind.
A model is always read only, so please NEVER NEVER NEVER use #bind.
#load is the highest annotation you could use, and this is only the case when you will create a new instance for the ListModel, witch is hardly needed.
Labels, are also not updatable in your GUI.
Again #bind is over the top, #load should be used in normal situations (when the value can change, so most commonly) or #init when the value will never change, but if you use #load I'll be happy already.
Hope this could set you to the right direction.
If you have any other question, just comment below.