My main page uses a TabbedPage to group existing news into different lists. The tabs aren't fixed; they're built from a data binding operations against a collection that's retried through a web service call.
I'd like to persist the selected tab across activity restarts, but it seems like I'm missing something. Since there's no selected tab property (which can be set through data binding), I've tried to handle the PageChanged and the CurrentPageChangedCommand events. I'm using the PageChanged to set the selected tab to the previous selected tab and the CurrentPageChangedCommand is being used to update the persisted selected tab (I'm using the Application.Properties to make sure the selected tab survives app restarts).
Unfortunately, the events generated by the tab will always set tab 0 as the selected tab! Here's what I'm seeing (let's assume that my app was killed white tab 3 was active):
When data is bound to the TabbedPage.ItemsSource property, the tab will automatically fire the CurrentPageChangedCommand, passing the first tab (tab at position 0).
My code handles the event and updates the current persisted selected tab by changing the selected tab in the Properties dictionary. So now, instead of 3 (which was the value persisted when my app was killed), it will have 0.
Then the tab will fire the PagesChanged
When my code handles this event, it will try to update the selected tab. However, when it access the selected tab from the Properties dictionary, it will get the default tab (0) and not 3. This happens because the CurrentPageChangedCommand was fired before the PagesChanged event (step 2), completely overriding the previously persisted tab index.
This default behaviour will also give a bad user experience when the user refreshes the current list (pull to refresh) because he always ends up seeing tab 0 list.
So, any clues on how to solve this? How have you guys solved this?
Thanks.
It seems it can't be achieved using MVVM as CurrentPage is not a bindable property and CurrentPageChanged is an event.
However, there's no need to handle the PagesChanged event. You could record the index in the changed event like:
private void MyTabbedPage_CurrentPageChanged(object sender, EventArgs e)
{
Application.Current.Properties["index"] = this.Children.IndexOf(CurrentPage);
Application.Current.SavePropertiesAsync();
}
Then you could set your tabbed page's current page after you have loaded all the tabs:
object index;
Application.Current.Properties.TryGetValue("index", out index);
if (index != null)
{
CurrentPage = Children[int.Parse(index.ToString())];
}
// Subscribe the event
CurrentPageChanged += MyTabbedPage_CurrentPageChanged;
I placed the code above in the custom tabbed page's constructor and it could change the selected tab at initial time.
Update:
If you want to change the tabbed page's children dynamically, you could define a property to avoid the event being fired when you change the children:
bool shouldChangeIndex = true;
private void MyTabbedPage_CurrentPageChanged(object sender, EventArgs e)
{
if (shouldChangeIndex)
{
var index = this.Children.IndexOf(CurrentPage);
Application.Current.Properties["index"] = index;
Application.Current.SavePropertiesAsync();
}
}
// Simulate the adjusting
shouldChangeIndex = false;
Children.Clear();
Children.Add(new MainPage());
Children.Add(new SecondPage());
shouldChangeIndex = true;
object index;
Application.Current.Properties.TryGetValue("index", out index);
if (index != null)
{
CurrentPage = Children[int.Parse(index.ToString())];
}
Unfortunately, I had to abandon the MVVM approach...In the end, I had to resort to code and a couple of flags to control when the generated tab events should be handled.
Related
I am building a Xamarin Forms mobile app that runs in Android on a Zebra scanner. I flip 2 different StackLayouts to IsVisble true/false to display different stuff in the UI. (StackLayout1 and StackLayout2)
The customer wants the user to be able to use the app entirely from the hardware keyboard on the scanner. So I have used the device Settings so that it never displays the virtual keyboard (I don’t think that matters for the issue I am having.)
I am overriding DispatchKeyEvent in a PageRenderer in the Android project and everything is working great … except.
The problem case:
StackLayout1 is displayed
the user taps an Entry control, putting the focus there
the user taps a button in the UI
the app displays StackLayout2
at this point the DispatchKeyEvent never fires no matter what key I press on the device keyboard
If an Entry box does NOT get the focus (step #2 above) the DispatchKeyEvent always fires in StackLayout2 and the StackLayouts display as expected.
If I programatically put the focus in an Entry box in StackLayout2 at step #3 above the DispatchKeyEvent fires fine.
That is not an OK solution. I have tried to progamatically put the focus on StackLayout2, and that code seems to do what is expected but DispatchKeyEvent does not fire.
Maybe I need to do something in the Android-project PageRenderer so that it is aware of StackLayout2 when it is made IsVisible = true.
Update 2: I found that I did NOT need custom StackLayouts. The solution which I posted below does not include any of this stuff I am describing in Update 1 (sorry, if that's confusing).
Update 1:
I added a ViewRenderer for both StackLayouts, and the code is hitting the OnElementChanged event when StackLayout2's IsVisible property flips to true, just great. Although the problem case is the same: DispatchKeyEvent does not fire once StackLayout2 is displayed, if an EntryBox had the focus in StackLayout1
Here is the OnElementChanged part of the new StackLayout ViewRenders
async void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case "IsVisible":
if (Element.IsVisible)
{
if (sender is StackLayout)
{
this.FocusableViewAvailable(this); // if I comment these 2 lines out I get the same bad result
this.Focusable = true; // if I comment these 2 lines out I get the same bad result
this.FocusableInTouchMode = true;
var dd = this.RequestFocus(); // this is always false
var ee = this.IsFocused; // this is always false
}
}
break;
}
}
Also, as I am pointing out in the comments ^ there, IsFocused is always false.
Ideas?
My hunch, "Maybe I need to do something in the Android project PageRenderer" was correct. In the DispatchKeyEvent I had to make the MainPage have the focus when the keypress was handled.
Here is what the DispatchKeyEvent looks like now (notice the comments):
public override bool DispatchKeyEvent(KeyEvent ke)
{
// MainPage.ReceiveKeyPress(e); is the method that this method returns to
bool KeyPressWasHandled = false;
KeyPressWasHandled = (Element as MainPage).ReceiveKeyPress(ke);
if (KeyPressWasHandled)
{
// this next block seems to be needed so that this class
// continues to receive the keypress event after an Entry box has had the focus
this.Focusable = true;
this.FocusableInTouchMode = true;
this.RequestFocus();
return true; // returning true tells the parent class that the keypress has been handled
} else
{
try
{
return base.DispatchKeyEvent(ke);
}
Now the "problem case" in my initial post is no longer a problem.
NOTE: I found that I did NOT need the custom ViewRenderers that I had made for the StackLayouts.
Here is what a portion of my screen looks like:
The user can pick a choice from the drop-down list and click the add button. Here is the code for the add button:
protected void btnModuleAdd_Click(object sender, EventArgs e)
{
var selectedModule = ddlModsList.SelectedItem.ToString();
var graphicName = this.GraphicName;
var xr = new GraphicModuleXRef();
xr.GraphicName = graphicName;
xr.Module = selectedModule;
// Take drop down list selection and add it to GraphicModuleXRef table.
var context = new XRefDataContext();
context.GraphicModuleXRefs.InsertOnSubmit(xr);
context.SubmitChanges();
}
Basically, it's taking the user's choice and writing it out to a table. This part works fine.
In my Page_Load, I check whether IsPostback and, if it is, I run the code below:
private void LoadOtherModulesUsed()
{
if (this.GraphicName != null)
{
lbModules.Items.Clear();
var context = new XRefDataContext();
var q = context.GraphicModuleXRefs
.Where(a => a.GraphicName.Contains(this.GraphicName));
foreach (GraphicModuleXRef gr in q)
{
lbModules.Items.Add(new ListItem(gr.Module.ToString()));
}
}
}
This code reads from a table, finds all records that match the criteria, and adds them to the listbox.
So, what I'm expecting to happen is for the page to reload and the listbox to be repopulated, including the new entry just added to the table. But, that isn't happening. The screen refreshes like it has reloaded, but the entry doesn't appear in the listbox. However, it IS there, it just can't be seen. If the user adds another entry, by clicking the Add button, the list 'rolls up' one row and the previous entry can be seen. But, not the new one. If the user exits from the screen and re-enters, all the entries in the listbox can be seen. It's almost like the listbox is too short to display all records, but I've tried different heights, with no difference.
I'm wondering if anyone can point me in the right direction?
Put simply, when adding a new item to the listbox, it isn't immediately visible unless another item is added, thereby 'rolling' the list up. Even scrolling the list with the scrollbar doesn't show the new entry until another entry is added. And, if you scroll the list up, you can see the prior entry. So strange!
EDIT: Trying to describe this more simply:
User adds item to listbox by pressing Add button.
New item does not appear in listbox.
User adds another item to listbox by press Add button.
Prior item now shows in listbox if user scrolls listbox up.
The newest item just added, however, does not appear unless step 3 is repeated.
Also, exiting the page and then coming back in loads every item in the list and all is visible.
This is a timing issue. Whats happening is Page_Load runs first in this case and THEN the Click event handler so effectively the control has been bound before the new entry is added. Thats why you're always one refresh behind. Id refactor your code like this so everything runs in the correct order! To understand the timing of event execution I strongly recommend reading this article on MSDN its AWESOME and will really help you get the best from ASP.NET.
Additionally reading this article on MSDN (Also awesome) especially the section on ViewState will explain how the DropDown retains its details even when, in the modified code, you're onlly filling it when the page is NOT a postback and when the click event is fired.
Hope this helps!
public void Page_Load(object sender, EventArgs e)
{
if (IsPostBack) return;
LoadOtherModulesUsed();
}
private void LoadOtherModulesUsed()
{
if (this.GraphicName != null)
{
lbModules.Items.Clear();
var context = new XRefDataContext();
var q = context.GraphicModuleXRefs
.Where(a => a.GraphicName.Contains(this.GraphicName));
foreach (GraphicModuleXRef gr in q)
{
lbModules.Items.Add(new ListItem(gr.Module.ToString()));
}
}
}
protected void btnModuleAdd_Click(object sender, EventArgs e)
{
var selectedModule = ddlModsList.SelectedItem.ToString();
var graphicName = this.GraphicName;
var xr = new GraphicModuleXRef();
xr.GraphicName = graphicName;
xr.Module = selectedModule;
// Take drop down list selection and add it to GraphicModuleXRef table.
var context = new XRefDataContext();
context.GraphicModuleXRefs.InsertOnSubmit(xr);
context.SubmitChanges();
LoadOtherModulesUsed();
}
Normally, you update the data bound to a datagrid item like this:
protected function onGridEditEnd(event:DataGridEvent):void
{
if (grid.dataProvider != null && event != null)
{
var editor:Object = event.currentTarget.itemEditorInstance;
if (event.columnIndex == getColumnIndex(columnA) {
collection[event.rowIndex].name = TextInput(editor).text;
}
}
}
The itemEditEnd event is dispatched right after the user finishes editing.
So, what if I want to
End the edit (from another method or handler other than itemEditEnd handler)
Update the value of the bound data item at the same time.
"while" the item is being edited by the user.
For example; how do I trigger this from a keyDown event handler?
Note: You could call onGridEditEnd(null) which would end the edit but not update the data. onGridEditEnd(new DataGridEvent(DataGridEvent.ITEM_EDIT_END, ...)) would work but you also need to store and pass values like columnIndex, rowIndex, dataField, etc.. which does not seem to be the best and simplest way.
thanks...
try setting editable property to false (then you can turn it back on).
Or you could try setting focus (FocusManager.focus = null) elsewhere. This way it will be still editable, but the user will have to click it again.
I'm building a custom server control derived from CompositeControl.
The control contains a number of child controls (Labels, DropDownList, ListSearchExtender, etc). All of them reside inside an UpdatePanel.
The control also publishes events. For this I added two Properties: EnableCallBacks and CallBacksAsPostBacks. Those two properties should configure the postback behaviour of the update panel.
Any ideas what a correct implementation should look like?
I'm getting some problems with the way I implemented it:
the PostBackTrigger does not always get rendered into the output html.
Having both Triggers.Add(trigger) and Controls.Add(_updatePanel) inside the CreateChildControls methods leads to the PostBackTrigger always being rendered, even if I remove it later on (e.g. within RenderControl() or PreRender()). If I do not add the trigger here but later on, then it does never get rendered. At this stage I do not have the correct values of all my properties yet (e.g. EnableCallBacks and CallBacksAsPostBacks).
It is not possible to place the statement of Controls.Add(_updatePanel) inside the RenderControl-method due to it beeing too late for AJAX (latest ist PreRender() otherwise I get an exception).
Ideally I would instantiate all controls in CreateChildControls() and then set their values later on in e.g. PreRender or RenderControl
Having both statements in the PreRender method results in, that the trigger gets rendered corretly depending on my settings in the containing page, but I don't get the DropDownList populated with its data from the ViewState (on call/postbacks).
protected override void CreateChildControls()
{
base.CreateChildControls();
_updatePanel = new UpdatePanel();
_updatePanel.ID = "FprDropDownList_UpPnl";
_updatePanel.UpdateMode = UpdatePanelUpdateMode.Conditional;
_label = new FprLabel();
_label.ID = "FprDropDownList_Lbl";
_updatePanel.ContentTemplateContainer.Controls.Add(_label);
_dropDownList = new DropDownList();
_dropDownList.ID = "FprDropDownList_Ddl";
_dropDownList.CssClass = "fprDropDownList";
_dropDownList.AutoPostBack = true;
_updatePanel.ContentTemplateContainer.Controls.Add(_dropDownList);
_label.AssociatedControlID = _dropDownList.ClientID;
_listSearchExtender = new ListSearchExtender();
_listSearchExtender.ID = "FprDropDownList_Lse";
_listSearchExtender.TargetControlID = _dropDownList.ClientID;
_listSearchExtender.PromptPosition = ListSearchPromtPosition;
_listSearchExtender.PromptCssClass = "fprListSearchExtender";
_updatePanel.ContentTemplateContainer.Controls.Add(_listSearchExtender);
_ddlPostBackTrigger = new PostBackTrigger();
_ddlPostBackTrigger.ControlID = _dropDownList.ClientID;
//_updatePanel.Triggers.Add(_ddlPostBackTrigger);
Controls.Add(_updatePanel);
}
protected override void OnPreRender(EventArgs pE)
{
if (EnableCallBacks)
{
_dropDownList.SelectedIndexChanged += DropDownList_SelectedIndexChanged;
}
if (EnableCallBacks && CallBacksAsPostBacks)
{
_updatePanel.Triggers.Add(_ddlPostBackTrigger);
}
//Controls.Add(_updatePanel);
base.OnPreRender(pE);
}
public override void RenderControl(HtmlTextWriter pWriter)
{
// Do some things... like set Enable-state of child controls
base.RenderControl(pWriter);
}
You should add your dynamic controls in PreInit for the events to fire properly.
Use this event for the following:
Check the IsPostBack property to
determine whether this is the first
time the page is being processed. The
IsCallback and IsCrossPagePostBack
properties have also been set at this
time.
Create or re-create dynamic
controls.
Set a master page
dynamically.
Set the Theme
property dynamically.
Read or set
profile property values.
I created a class CustomCombo.as that extends ComboBox. What is happening is that the CustomCombo combobox is showing as being editable. I do not want this and I cant find the properties to set the editable to false.
I also tried setting the combobox's textInput.editable control to false, but to no avail.
Any help would be greatly appreciated.
CustomCombo.as
package custom {
import spark.components.ComboBox;
public class CustomCombo extends ComboBox {
public function CustomCombo() {
super();
// this.editable = false; //<-- THIS DOESNT WORK ***Access of possibly undefined property editable through a reference with static type custom:CustomCombo
// this.textInput.editable = false; //<-- THIS DOESNT WORK ***Cannot access a property or method of a null object reference
}
}
}
After rummaging through the Flex 4 API I found that they suggest to use the DropDownList control. From what I can see is that they removed the editable property from the ComboBox control in Flex 4, but I may be wrong.
I implemented DropDownList and that solved my problem.
I see that you're using spark and not mx. The editable property I referred to is applicable only to mx based list. In spark, ComboBox extends DropDownListBase and is editable by default.
The ComboBox control is a child class of the DropDownListBase control. Like the DropDownListBase control, when the user selects an item from the drop-down list in the ComboBox control, the data item appears in the prompt area of the control.
One difference between the controls is that the prompt area of the ComboBox control is implemented by using the TextInput control, instead of the Label control for the DropDownList control. Therefore, a user can edit the prompt area of the control to enter a value that is not one of the predefined options.
For example, the DropDownList control only lets the user select from a list of predefined items in the control. The ComboBox control lets the user either select a predefined item, or enter a new item into the prompt area. Your application can recognize that a new item has been entered and, optionally, add it to the list of items in the control.
The ComboBox control also searches the item list as the user enters characters into the prompt area. As the user enters characters, the drop-down area of the control opens. It then and scrolls to and highlights the closest match in the item list.
So ideally, you should be using DropDownList in this case.
You're getting null error when trying to access textInput from the constructor because it hasn't been created yet. In mx based controls (Flex-3), you can access it from the creationComplete handler; I'm not quite sure how to do it for spark based controls.
Update: I think I've figured out how to access skin parts in spark (though you might wanna use the DropDownBox instead). You have to override the partAdded method.
override protected function partAdded(partName:String, instance:Object):void
{
super.partAdded(partName, instance);
if (instance == textInput)
{
textInput.editable = false;
}
}
There's one catch though: it may not work in this case. The source code of ComboBox.as says that
the API ignores the visual editable and selectable properties
So DropDownList it is!
Initial answer, posted for mx ComboBox.
This shouldn't happen as the default value of the editable property is false.
Try explicitly setting the value to false from the constructor.
public function CustomCombo() {
super();
this.editable = false;
}