Xamarin.Forms Shell generate a Top Tapped Page in Code behind - xaml

thanks for your help in advance. I am working on a Forms Shell App a few days now. My UI is data driven in a lot of aspects and what I am trying to accomplish ist a page that show multiple tabs on tob depending on the model it has.
In other words I try to accomplish the same layout as in the Shells Demo app:
<FlyoutItem Route="animals"
Title="Animals"
FlyoutDisplayOptions="AsSingleItem">
<Tab Title="Domestic"
Route="domestic"
Icon="paw.png">
<ShellContent Route="cats"
Style="{StaticResource DomesticShell}"
Title="Cats"
Icon="cat.png"
ContentTemplate="{DataTemplate views:CatsPage}" />
<ShellContent Route="dogs"
Style="{StaticResource DomesticShell}"
Title="Dogs"
Icon="dog.png"
ContentTemplate="{DataTemplate views:DogsPage}" />
</Tab>
</FlyoutItem>
But starting from an existing link in the FlyoutMenu. Like the level of the "Domestic" tab above, which when navigatet to creates those two or more Shell top tapped pages.
I am aware I could use a TappedPage as ShellContent, but TappedPages get styled a little different I was wondering if i could accomplish this with "native" Shell features.
I already tried something like:
ShellSection tab1 = new ShellSection
{
Title = "Tab1"
};
tab1.Items.Add(new ShellContent() { Title = "Test1", Content = new ContentPage() { Title = "Test1" } });
tab1.Items.Add(new ShellContent() { Title = "Test2", Content = new ContentPage() { Title = "Test2" } });
var current = AppShell.Current.CurrentItem;
current.Items.Add(tab1);
but this also adds another entry in the bottom tabs bar and the flyout menu as well, what i don't want.
To be more clear I try to explain it with mockup screenshots.
Asume we have the following flyoutmenu where Domestic entry.
This domestic entry leads to a page whichs viewmodel provides a collection of data that should be displayed in tabs.
As you see, this pages provides a Link in the title view which opens a popup to change the selected element and should refresh/change the displayed data for example to have three tabs.
I hope its more clear now.
EDIT:
The comments till now brought me one step further. With the following code I accomplish to get the desired top tabs.
var current = AppShell.Current.CurrentItem;
var currentSection = current.CurrentItem;
//currentSection.Items.Clear();
currentSection.Items.Add(new CatsPage() { Title = "Tab1"} ); ;
currentSection.Items.Add(new ShellContent() { Title = "Tab2", Content = new CatsPage() });
But its still not perfect - in the added pages the botton tab bar is hidden, any solution for that?
Has someone an advice?

This solution also includes the usage of the ContentTemplate in code:
AppShell.xaml
<?xml version="1.0" encoding="utf-8"?>
<Shell
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Namespace.To.AppShell.xaml.cs"
FlyoutBehavior="True" />
AppShell.xaml.cs
public AppShell()
{
this.InitializeComponent();
var flyoutTabOne = new Tab()
{
Items =
{
new ShellContent
{
ContentTemplate = new DataTemplate(() => new YourPage()),
Route = "Route.To.Your.Page",
Title = "Your page title",
},
},
Route = "Route.To.Your.Tab.One",
Title = "Tab one title",
};
var flyoutItem = new FlyoutItem();
flyoutItem.Items.Add(flyoutTabOne);
this.Items.Add(flyoutItem);
}
The usage of ContentTemplate is important to obtain lazy loading of all tabs. Therefore I strongly recommend to use a ContentTemplate whenever possible and as it was already the case in the opening question (in xaml). If you are interested in more information about the ContentTemplate, please read this documentation: https://learn.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/shell/tabs#efficient-page-loading

Do you mean you want to achieve the same structure as in the Shell demo app by behind codes ?
If yes,you could try this :
in AppShell.xaml.cs:
public AppShell()
{
InitializeComponent();
FlyoutItem shellItem = new FlyoutItem() { Title = "Tab1" };
Tab tab = new Tab();
tab.Items.Add(new ShellContent() { Title = "Test1", Content = new Page1()});
tab.Items.Add(new ShellContent() { Title = "Test2", Content = new Page2()});
shellItem.Items.Add(tab);
this.Items.Add(shellItem);
}
AppShell.xaml :
<?xml version="1.0" encoding="UTF-8"?>
<Shell xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
xmlns:local="clr-namespace:ShellDemo.Views"
Title="ShellDemo"
x:Class="ShellDemo.AppShell">
</Shell>
page1.xaml:
<ContentPage ...>
<Shell.TitleView>
<Button Text="change" Clicked="Button_Clicked"></Button>
</Shell.TitleView>
...
</ContentPage>
in page1.xaml.cs:
private void Button_Clicked(object sender, EventArgs e)
{
Tab tab = new Tab();
tab.Items.Add(new ShellContent() { Title = "Test3", Content = new ContentPage() { Title = "Test3" } });
tab.Items.Add(new ShellContent() { Title = "Test4", Content = new ContentPage() { Title = "Test4" } });
AppShell.Current.CurrentItem.Items.Clear();
AppShell.Current.CurrentItem.Items.Add(tab);
}
Update1:
from your sketches above,do you want to change the TabbedPage content after click the link in titleview ()?
the effec is like below (you should add shell titileview in every page,here use buttons instead of the link):
Update 1 you only need to change the tab's contents:
private void Button_Clicked(object sender, EventArgs e)
{
Tab tab = (Tab)AppShell.Current.CurrentItem.CurrentItem;
tab.Items.Clear();
tab.Items.Add(new ShellContent() { Title = "Test3", Content = new ContentPage() { Title = "Test3" } });
tab.Items.Add(new ShellContent() { Title = "Test4", Content = new ContentPage() { Title = "Test4" } });
}
Update 2:
I download your project and test it.It doesn't actually hide the bottom Tab, you can click on the bottom Tab and see that it exists, but it doesn't have a background color, so you just give it the Style that defined in Shell.Resources when you add ShellContent
var current = AppShell.Current.CurrentItem;
var currentSection = current.CurrentItem;
currentSection.Items.Add(new ShellContent() { Title = "Test3", Content = new CatsPage(),Style = (Style)AppShell.Current.Resources["BearsShell"] }); ;
currentSection.Items.Add(new ShellContent() { Title = "Test4", Content = new CatsPage(), Style = (Style)AppShell.Current.Resources["BearsShell"] });

Related

Titanium Alloy: Actionbar & DrawerMenu

Recently I started working on a multi-platform application using Titanium Alloy.
One of the things I'd like to achieve is having a menu button in the actionbar (infront of the appicon).
When pressed, it toggles the drawermenu.
After a little investigation I failed to find a widget / module that could offer me both. So I decided to use a combination of com.alcoapps.actionbarextras and com.mcongrove.slideMenu.
Both a custom actionbar and a drawer option seem to functionate as they appear they should.
The problem is however, that it does show the 'menu' image, it is clickable, but I have no idea how to attach an event to it.
I've tried several ways, like binding the event to the entire actionbar of what so ever.. But I can't seem to find the appropriate binding to accomplish this.
index.js
var abextras = require('com.alcoapps.actionbarextras');
$.MainWindow.on('open', function(evt) {
// set extras once the Activity is available
abextras.title = "Test Window";
abextras.homeAsUpIcon = "/images/menu.png";
//abextras.titleColor = "#840505";
//abextras.subtitle = "for some extra action";
//abextras.subtitleFont = "Chunkfive.otf";
//abextras.subtitleColor = "#562A2A";
//abextras.backgroundColor = "#F49127";
var activity = evt.source.activity;
if (activity) {
activity.onCreateOptionsMenu = function(e) {
e.menu.clear();
activity.actionBar.displayHomeAsUp = true;
//abextras.setHomeAsUpIcon("/images/menu.png");
//activity.actionBar.addEventListener("click", function(ev) {
// console.log("HI");
//});
};
}
/*
// now set the menus
evt.source.activity.onCreateOptionsMenu = function(e) {
// aboutBtn and creditsBtn will be displayed in the menu overflow
aboutBtn = e.menu.add({
title : "About",
showAsAction : Ti.Android.SHOW_AS_ACTION_NEVER
});
aboutBtn.addEventListener("click", function(e) {
console.log('Clicked on About');
});
creditsBtn = e.menu.add({
title : "Credits",
showAsAction : Ti.Android.SHOW_AS_ACTION_NEVER
});
creditsBtn.addEventListener("click", function(e) {
console.log('Clicked on Credits');
});
// create the Share intent and add it to the ActionBar
var intent = Ti.Android.createIntent({
action : Ti.Android.ACTION_SEND,
type : 'text/plain'
});
intent.putExtra(Ti.Android.EXTRA_TEXT, 'Hello world!');
abextras.addShareAction({
menu : e.menu,
//intent : intent
});
};
*/
});
function doClick(e) {
alert($.label.text);
}
// Create our node items
var nodes = [{
menuHeader : "My Tabs",
id : 0,
title : "Home",
image : "/images/home.png"
}, {
id : 1,
title : "Contact",
image : "/images/phone.png"
}, {
id : 2,
title : "Settings",
image : "/images/gear.png"
}];
// Initialize the slide menu
$.SlideMenu.init({
nodes : nodes,
color : {
headingBackground : "#000",
headingText : "#FFF"
}
});
// Set the first node as active
$.SlideMenu.setIndex(0);
// Add an event listener on the nodes
$.SlideMenu.Nodes.addEventListener("click", handleMenuClick);
// Handle the click event on a node
function handleMenuClick(_event) {
if ( typeof _event.row.id !== "undefined") {
// Open the corresponding controller
openScreen(_event.row.id);
}
}
function openMenu() {
$.AppWrapper.animate({
left : "300dp",
duration : 250,
curve : Ti.UI.ANIMATION_CURVE_EASE_IN_OUT
});
$.SlideMenu.Wrapper.animate({
left : "0dp",
duration : 250,
curve : Ti.UI.ANIMATION_CURVE_EASE_IN_OUT
});
toggleMenu();
}
function closeMenu() {
$.AppWrapper.animate({
left : "0dp",
duration : 250,
curve : Ti.UI.ANIMATION_CURVE_EASE_IN_OUT
});
$.SlideMenu.Wrapper.animate({
left : "-300dp",
duration : 250,
curve : Ti.UI.ANIMATION_CURVE_EASE_IN_OUT
});
toggleMenu();
}
function toggleMenu() {
//
console.log($.AppWrapper.left);
}
$.AppWrapper.addEventListener("swipe", function(_event) {
if (_event.direction == "right") {
openMenu();
} else if (_event.direction == "left") {
closeMenu();
}
});
$.MainWindow.open();
//$.index.open();
index.xml
<Alloy>
<Window class="container" id="MainWindow">
<Require type="widget" src="com.mcongrove.slideMenu" id="SlideMenu" />
<View id="AppWrapper">
<Label text="Profile View" />
</View>
</Window>
</Alloy>
I hope people with more knowledge about Titanium and/or these modules could guide me.
Kind Regards, larssy1
After contacting the creator of the widget, the outcome is as the following:
var activity = evt.source.activity;
if (activity){
activity.actionBar.onHomeIconItemSelected = function() {
// your callback here
alert('I was clicked');
}
}

Backbone Model object passed between Alloy controllers have null properties

I've started using titanium yesterday and i'm using the 'Creating your first titanium app' tutorial (this one).
When it gets on the part that you click on a row and it opens another view, my argument values are null. Here's my code:
Bookdetails.js
var args = arguments[0] || {};
$.titleLabel.text = args.Title || 'Default Title';
$.authorLabel.text = args.Author || 'Default Author';
index.js
var myBooks = Alloy.Collections.Books;
var myBook = Alloy.createModel('Books', {Title:'Bleach',Author: 'Kubo Tite'});
myBooks.add(myBook);
myBook.save();
function showBook(event){
var selectedBook = event.source;
var args = {'Title': selectedBook.Title, 'Author': selectedBook.Author};
var bookDetails = Alloy.createController("Bookdetails",args).getView();
bookDetails.open();
}
$.index.open();
Bookdetails.xml
<Alloy>
<Window class="container">
<Label id="titleLabel"></Label>
<Label id="authorLabel"></Label>
</Window>
</Alloy>
And my Books (model) definition:
exports.definition = {
config: {
columns: {
"Title": "text",
"Author": "text"
},
adapter: {
type: "sql",
collection_name: "Books"
}
},
extendModel: function(Model) {
_.extend(Model.prototype, {
// extended functions and properties go here
});
return Model;
},
extendCollection: function(Collection) {
_.extend(Collection.prototype, {
// extended functions and properties go here
});
return Collection;
}
};
Index.xml
<Alloy>
<Collection src="Books" />
<Window class="container">
<TableView dataCollection="Books">
<TableViewRow onClick="showBook" title="{Title}"></TableViewRow>
</TableView>
<Menu id="menu" platform="android">
<MenuItem id="addBooks" title="Adicionar" onClick="callAddBook" showAsAction="Ti.Android.SHOW_AS_ACTION_IF_ROOM" />
</Menu>
</Window>
</Alloy>
When i run my app, it show 'Default Author' and 'Default Title'. I'm running the app on android (samsung s3)
When you are handling event triggered by TableView in event.source you don't get Backbone Object which was used to generate for that row but Titanium.UI.TableViewRow object.
Also it's better to access event.row instead of event.source when you are operating on TableView (check documentation to see difference).
To make this example work just change showBook function to:
function showBook(event){
var selectedBook = myBooks.where({Title: event.row.title})[0];
var args = {'Title': selectedBook.get('Title'), 'Author': selectedBook.get('Author')};
var bookDetails = Alloy.createController("Bookdetails", args).getView();
bookDetails.open();
}
Also instead of creating new args object and sending it to Bookdetails controller you can send unmodified Backbone Model object to make easier any further modifications on it:
function showBook(event){
var selectedBook = myBooks.where({Title: event.row.title})[0];
var bookDetails = Alloy.createController("Bookdetails", selectedBook).getView();
bookDetails.open();
}
Bookdetails.js
var args = arguments[0] || {};
$.titleLabel.text = args.get('Title') || 'Default Title';
$.authorLabel.text = args.get('Author') || 'Default Author';

TableView 'click' listener being ignored

I am dynamically building a TableView in my controller, which works fine: The initial table displays the initial collections data as expected.
The problem is that the TableView's 'click' event listener is ignored when I click on the table rows. I am testing in the browser, and I never even see the console event file (see comments in controller file). All relevant code snippets below:
In my alloy.js I setup a backbone collection:
function defaultTodo(name) { return {name: name, done: false}; }
function doneTodo(name) { return {name: name, done: true}; }
Alloy.Collections.todos = new Backbone.Collection();
Alloy.Collections.todos.reset([
defaultTodo('Apples'), // create not yet done todo
defaultTodo('Banana'),
defaultTodo('Paper Towels'),
defaultTodo('Broccoli'),
doneTodo('Beans'), // create already done todo
doneTodo('Water'),
doneTodo('Blueberries'),
doneTodo('Stir Fry')
])
Here is my index.js controller:
var todos = Alloy.Collections.todos;
function redrawTable() {
// clear all the old data
// See http://developer.appcelerator.com/question/49241/delete-all-rows-in-a-tableview-with-a-single-click
$.table.setData([]);
// Create and add the TableViewSections
var alreadyDone = Ti.UI.createTableViewSection({ headerTitle: "Already Done" });
var needsDoing = Ti.UI.createTableViewSection({ headerTitle: "Needs Doing" });
$.table.appendSection(needsDoing);
$.table.appendSection(alreadyDone);
// Add the todo to the appropriate sections
todos.forEach(function(todo) {
var section = todo.get('done') ? alreadyDone : needsDoing;
addEntry(todo, section);
});
// Add the click listener
// THIS LISTENER IS IGNORED ********************************
$.table.addEventListener('click', function(e) {
console.log(e);
todos.at(e.index).set('done', true);
todos.trigger('change');
});
// Helper function to add a row to a section
function addEntry(todo, section) {
var row = Ti.UI.createTableViewRow({
title: todo.get('name'),
className: "row"
});
section.add(row);
}
}
// Redraw our table each time our todos collection changes
todos.on('change', redrawTable);
// Trigger a change event to draw the initial table
todos.trigger('change');
$.index.open();
And here is index.xml view file:
<Alloy>
<Window class="container">
<Label id="test" class="header">My Grocery List</Label>
<TextField id="newItem"/>
<TableView id="table">
</TableView>
</Window>
</Alloy>
UPDATE: Working Code
In addition to the changes below, I also added onClick="markDone" to the xml.
function markDone(e) {
console.log(e.row.todo);
e.row.todo.set('done', true);
todos.trigger('change');
}
function redrawTable() {
// clear all the old data
// See http://developer.appcelerator.com/question/49241/delete-all-rows-in-a-tableview-with-a-single-click
$.table.setData([]);
var rows = [];
var done = [];
var doing = [];
// Add the todo to the appropriate sections
todos.forEach(function(todo) {
var row = Ti.UI.createTableViewRow({
title: todo.get('name'),
className: "row"
});
row.todo = todo;
todo.get('done') ? done.push(row) : doing.push(row);
});
// Create and add the TableViewSections
rows.push(Ti.UI.createTableViewSection({ headerTitle: "Needs Doing" }));
rows = rows.concat(doing);
rows.push(Ti.UI.createTableViewSection({ headerTitle: "Already Done" }));
rows = rows.concat(done);
$.table.setData(rows);
};
I created brand new project using files which you provided and eventListener is working perfectly fine. However there are couple other bugs:
Creating listener inside redrawTable() function, which is executed every time you click on something in TableView. As a result at the beginning you have one eventListener but after every click all listeners are duplicated.
Using index property in event handler to find index of Backbone model object to update. index property is indicating at which place given row was displayed in your TableView. When you are moving rows between sections their index are changing. In your case it's better to check e.row.name property and use Backbone.Collection.findWhere() method. If user can have two items with the same name on the list, then you have to create additional property to determine which object in model should be changed.
You should add rows to section before section are added to table. In your case table is very simple so instead of doing loops you can just create one simple array of objects (with title and optional header properties) and pass it to $.table.setData().
It's good to wait for postlayout event triggered on main view before triggering any custom events to make sure that the whole view was created and all objects are initiated.
Check rewrote index.js code below to see how it could be done.
var todos = Alloy.Collections.todos;
function redrawTable() {
var done = todos.where({done: true}).map(function(elem) {
return { title: elem.get('name') };
});
if (done.length > 0) {
done[0].header = 'Already Done';
}
var notdone = todos.where({done: false}).map(function(elem) {
return { title: elem.get('name') };
});
if (notdone.length > 0) {
notdone[0].header = 'Needs Doing';
}
$.table.setData( done.concat(notdone) );
}
$.table.addEventListener('click', function(e) {
todos.where({ name: e.row.title }).forEach(function(elem){
elem.set('done', !elem.get('done'));
});
todos.trigger('change'); // Redraw table
});
// Redraw our table each time our todos collection changes
todos.on('change', redrawTable);
// Trigger a change event to draw the initial table
$.index.addEventListener('postlayout', function init(){
todos.trigger('change');
// Remove eventListener, we don't need it any more.
$.index.removeEventListener('postlayout', init);
})
$.index.open();
redrawTable() could have a little more refactor but I left it so it's easier to read.
To read more about manipulating TableView check this Appcelerator documentation page.

Dojo DropDownButton, can I differentiate between a click on button vs. down arrow?

I'm using Dojo to create a DropDownButton within a Toolbar. The Toolbar, and button are created dynamically, like this:
this.widget = new Toolbar({ style: "background:black;" }, "toolbar");
this.dropMenu = new DropDownMenu({tooltip : "ToolTip", style: "display: none;"});
this.button = new DropDownButton({dropDown: this.dropMenu});
this.button.set('label', '<img src="data:image/png;base64,'+ this.icon + '"/>');
this.widget.addChild(this.button);
Note that the above code is dynamically creating an icon as part of the button from a base64 encoded string through setting an img src for the label property of the button.
I want to differentiate between a click on the "label" element for the DropDownButton and a click on the down arrow for the button, but am not sure if this is possible. Ie, when clicking on the label, I capture the onClick, but don't cause the drop down to be displayed. However, if the down arrow is clicked on or any other place on the button is clicked, the drop down will be displayed.
One alternate would be to split this into a standard Button, and then a drop down button adjacent to it, but I'm wondering if there is any way to do this from a single standard DropDownButton?
Check whether or not its the downarrow or buttontext class in the clicked element. To properly hook into the 'flow' of events, you should override the classfunction _onDropDownMouseDown
var customDropDownButton = declare("customDropDownButton", [ DropDownButton ], {
toggleDropDown: function() {
console.log('toggling');
this.inherited(arguments);
},
_onDropDownMouseDown: function(evt) {
console.log(arguments, evt.srcElement.className);
if (/dijitButtonText/.test(evt.srcElement.className)) {
// negate popup functionality
console.log('negating');
return false;
}
this.inherited(arguments);
return true;
}
});
var b = new customDropDownButton({
label: "hello!",
name: "programmatic1",
dropDown: someMenu
});
Alternatively, if you can live with popup showing and then immediately closing again - easy way is:
var b = new DropDownButton({
label: 'hello!',
name: "programmatic2",
dropDown: someMenu,
onClick: function(evt) {
if(/dijitButtonText/.test(evt.srcElement.className)) {
// negate popup
popup.close(this.dropDown);
}
}
}, 'button');

Ext JS 4 - how to change content of an item which can't be referenced by an ID

I would really appreciate any help with the following problem:
I need to be able to change content of an item (div or textfield) but the problem is that there are going to be multiple instances of the same window so I cannot use div IDs.
I tried this little example:
var myBtnHandler = function(btn) {
myPanel.items.items[0].html = "Changed by click!";
myPanel.doLayout();
}
var fileBtn = new Ext.Button({
text : 'Change',
handler : myBtnHandler
});
var panel1 = {
html : 'Original content.'
};
var myPanel = new Ext.Window({
title : 'How to change it?',
items : [
panel1,
fileBtn
]
});
myPanel.items.items[0].html = "Changed on load!";
myPanel.show();
Referencing an element by myPanel.items.items[0] works on load but does not work when it's in the button handler - is it a scope-related problem? How to reference an element without its ID?
Thank you very much,
H.
The problem has nothing to do with scope. The first time you set the html property, the component has not yet been rendered, so on initial render it will read the html property off the component. The second time, you're just setting a property on an object, it's not going to react in any way.
Instead, you should use the update() method.
Ext.require('*');
Ext.onReady(function() {
var myBtnHandler = function(btn) {
myPanel.items.first().update("Changed by click!");
}
var fileBtn = new Ext.Button({
text: 'Change',
handler: myBtnHandler
});
var panel1 = {
html: 'Original content.'
};
var myPanel = new Ext.Window({
title: 'How to change it?',
items: [panel1, fileBtn]
});
myPanel.items.items[0].html = "Changed on load!";
myPanel.show();
});
There are several functions that go through elements that belongs to a container. Try using for example down():
btn = panel.down('button');
Where 'button' parameter would mean 'give me element which type is equal to 'button'. Check out Sencha doc for querying various elements too: http://docs.sencha.com/ext-js/4-0/#!/api/Ext.ComponentQuery
Following on from Sha's reply. To put his advice in context with your example.
var myBtnHandler = function(btn) {
btn.up('window').down('panel').update('Changed by click!');
}
var fileBtn = new Ext.Button({
text : 'Change',
handler : myBtnHandler
});
var panel1 = {
html : 'Original content.'
};
var myPanel = new Ext.Window({
title : 'How to change it?',
items : [
panel1,
fileBtn
]
});
myPanel.show();
myPanel.update('Changed on load!');