UISearchController and MvvmCross - c#

I want add search logic for my application (IOS8). I have simple MvxTableViewController and display my data by UITableViewSource. Here is:
...controller:
MvxViewFor(typeof(MainViewModel))]
partial class MainController : MvxTableViewController
{
public MainController(IntPtr handle) : base(handle) { }
public override void ViewDidLoad()
{
base.ViewDidLoad();
// make background trasnsparent page
this.View.BackgroundColor = UIColor.Clear;
this.TableView.BackgroundColor = UIColor.Clear;
this.NavigationController.NavigationBar.BarStyle = UIBarStyle.Black;
this.SetBackground ();
(this.DataContext as MainViewModel).PropertyChanged += this.ViewModelPropertyChanged;
}
private void SetBackground()
{
// set blured bg image
}
private void ViewModelPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
var viewModel = this.ViewModel as MainViewModel;
if (e.PropertyName == "Title")
{
this.Title = viewModel.Title;
}
else if (e.PropertyName == "Topics")
{
var tableSource = new TopicTableViewSource(viewModel.Topics);
tableSource.TappedCommand = viewModel.NavigateToChildrenPageCommand;
this.TableView.Source = tableSource;
this.TableView.ReloadData();
}
}
I read about search in IOS and choosed UISearchController for IOS8 app. But I don't understand, how I can add this controller to my view :(
I found sample from Xamarin (TableSearch) - but they don't use UITableViewSource and I don't understand what I should do with this.
I tried add controller:
this.searchController = new UISearchController (this.searchTableController)
{
WeakDelegate = this,
DimsBackgroundDuringPresentation = false,
WeakSearchResultsUpdater = this,
};
this.searchController.SearchBar.SizeToFit ();
this.TableView.TableHeaderView = searchController.SearchBar;
this.TableView.WeakDelegate = this;
this.searchController.SearchBar.WeakDelegate = this;
what should I do in this.searchTableController? Do I need move my display logic there?

Yes. The "searchTableController" should be responsible for the presentation of search results.
Here is the test project (native, not xmarin) which help you understand.
The searchController manages a "searchBar" and "searchResultPresenter". His not need add to a view-hierarchy of the carrier controller. When user starts typing a text in the "searchBar" the "SearchController" automatically shows your SearchResultPresenter for you.
Steps:
1) Instantiate search controller with the SearchResultsPresenterController.
2) When user inputs text in the search-bar you should invoke a your own service for the search. Below a sample of code..
#pragma mark - UISearchResultsUpdating
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController
{
NSString *searchString = searchController.searchBar.text;
if (searchString.length > 1)
{
// TODO - call your service for the search by string
// this may be async or sync
// When a data was found - set it to presenter
[self.searchResultPresenter dataFound:<found data>];
}
}
3) In the search presenter need to reload a table in the method "dataFound:"
- (void)dataFound:(NSArray *)searchResults
{
_searchResults = searchResults;
[self.tableView reloadData];
}

Here are some advice on how to use the UISearchController with Xamarin.iOS.
Create a new class for the results table view subclassing UITableViewSource. This is gonna be the view responsible of displaying the results. You need to make the items list of that table view public.
public List<string> SearchedItems { get; set; }
In your main UIViewController, create your UISearchController and pass your result table view as an argument. I added some extra setup.
public UISearchController SearchController { get; set; }
public override void ViewDidLoad ()
{
SearchController = new UISearchController (resultsTableController) {
WeakDelegate = this,
DimsBackgroundDuringPresentation = false,
WeakSearchResultsUpdater = this,
};
SearchController.SearchBar.SizeToFit ();
SearchController.SearchBar.WeakDelegate = this;
SearchController.HidesNavigationBarDuringPresentation = false;
DefinesPresentationContext = true;
}
The best way to add the search bar to your UI in term of user experience, in my opinion, is to add it as a NavigationItem to a NavigationBarController.
NavigationItem.TitleView = SearchController.SearchBar;
Add methods to perform the search in the main UIViewController:
[Export ("updateSearchResultsForSearchController:")]
public virtual void UpdateSearchResultsForSearchController (UISearchController searchController)
{
var tableController = (UITableViewController)searchController.SearchResultsController;
var resultsSource = (ResultsTableSource)tableController.TableView.Source;
resultsSource.SearchedItems = PerformSearch (searchController.SearchBar.Text);
tableController.TableView.ReloadData ();
}
static List<string> PerformSearch (string searchString)
{
// Return a list of elements that correspond to the search or
// parse an existing list.
}
I really hope this will help you, good luck.

Related

Enumerating Child Classes for Selection

I am making an editor tool that allows me to add parts to instances of scriptable-object class I've made. I use a generic method to add the new parts:
[CreateAssetMenu]
class Whole : ScriptableObject {
List<PartBase> parts = new();
public void AddPart<T>() where T:PartBase, new() { parts.Add(new T()); }
}
class Foo {
//insert selection statement that goes through each Child class of PartBase
}
The way I'm currently doing what I intend to do looks like this:
switch (EPartType)
{
case EPartType.Bar:
AddPart<BarPart>();
case EPartType.Baz:
AddPart<BazPart>();
case EPartType.Bor:
AddPart<BorPart>();
default:
break;
}
I am wondering if there's a way to do this that doesn't require me to switch on every single sub-class of PartBase (so that users can add their own custom parts just by making a new script inheriting from PartBase and not having to additionally tinker with my safekept enum), while still providing some sort of enumerated selection that can be used as a drop-down in my tool.
Thank you in advance!
I know this goes a bit beyond the scope of your question but here you go.
The comments in the code should hopefully explain every step
[CreateAssetMenu]
public class Whole : ScriptableObject
{
public List<PartBase> parts = new();
#if UNITY_EDITOR
// A custom Inspector for this type to extend it with an additional button
[CustomEditor(typeof(Whole))]
private class WholeEditor : Editor
{
// serialized property for the "parts"
private SerializedProperty m_PartsProperty;
// tiny hack to simulate a dropdown (see below)
private Rect _rect;
private void OnEnable()
{
// link up serialized property
m_PartsProperty = serializedObject.FindProperty(nameof(parts));
}
public override void OnInspectorGUI()
{
// draw the default inspector
base.OnInspectorGUI();
EditorGUILayot.Space();
var buttonClicked = GUILayout.Button("Add Part");
if (Event.current.type == EventType.Repaint)
{
// little hack to get the button rect in order to place
// our popup window here to kinda simulate a dropdown
_rect = GUIUtility.GUIToScreenRect(GUILayoutUtility.GetLastRect());
}
if (buttonClicked)
{
// Get the FOLDER where the current main asset is placed
// we will place created part assets here as well
var mainAssetPath = AssetDatabase.GetAssetPath(target);
var parts = mainAssetPath.Split('/').ToList();
parts.RemoveAt(parts.Count - 1);
mainAssetPath = string.Join('/', parts);
// shift the target position lower to start the window right under the "Add Part" button
_rect.y += _rect.height;
WholeEditorAddPopup.OpenPopup(_rect, m_PartsProperty, mainAssetPath);
}
}
private class WholeEditorAddPopup : EditorWindow
{
// The property where to finally add the created part
private SerializedProperty m_ListProperty;
// available Types and their according display names
private Type[] m_AvailableTypes;
private GUIContent[] m_DisplayOptions;
// just the label for the dropdown
private readonly GUIContent m_Label = new("Part to add");
// Folder where to create assets
private string m_MainAssetPath;
// currently selected type index
private int m_Selected = -1;
public static void OpenPopup(Rect buttonRect, SerializedProperty listProperty, string mainAssetPath)
{
// create a new instance of this window
var window = GetWindow<WholeEditorAddPopup>(true, "Add Part");
// assign the fields
window.m_ListProperty = listProperty;
window.m_MainAssetPath = mainAssetPath;
// get all assemblies
window.m_AvailableTypes = AppDomain.CurrentDomain.GetAssemblies()
// get all Types
.SelectMany(assembly => assembly.GetTypes())
// Filter to only have non-abstract child classes of "PartBase"
.Where(type => type.IsSubclassOf(typeof(PartBase)) && !type.IsAbstract)
// order by "FullName" (=> including namespaces)
.OrderBy(type => type.FullName)
.ToArray();
// For the display names replace all "." by "/"
// => Unity treats those as nested folders in the popup (see demo below)
window.m_DisplayOptions = window.m_AvailableTypes.Select(type => new GUIContent(type.FullName.Replace('.', '/'))).ToArray();
// show as Dropdown -> clicking outside automatically closes window
window.ShowAsDropDown(buttonRect, new Vector2(buttonRect.width, EditorGUIUtility.singleLineHeight * 4));
// [optional] set position again since "ShowAsDropDown" might have hanged it
window.position = new Rect(buttonRect.x, buttonRect.y, buttonRect.width, EditorGUIUtility.singleLineHeight * 4);
}
private void OnGUI()
{
// Draw a dropdown button containing all the available types
// grouped by namespaces and ordered alphabetically
m_Selected = EditorGUILayout.Popup(m_Label, m_Selected, m_DisplayOptions);
// only enable the "Add" button if valid index selected
var blockAdd = m_Selected < 0;
EditorGUILayout.Space();
using (new EditorGUI.DisabledScope(blockAdd))
{
if (GUILayout.Button("Add"))
{
// get selected type by selected index
var selectedType = m_AvailableTypes[m_Selected];
// create runtime ScriptableObject instance by selected type
var part = CreateInstance(selectedType);
// Set its initial name
part.name = $"new {selectedType.Name}";
// Get a unique path for this asset
// => if already an asset with same name Unity adds an auto-incremented index
var path = AssetDatabase.GenerateUniqueAssetPath($"{m_MainAssetPath}/{part.name}.asset");
// Create the asset, save and refresh
AssetDatabase.CreateAsset(part, path);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
// not sure anymore but from an old experience I think you need to re-load the asset
var loadedPart = AssetDatabase.LoadAssetAtPath<PartBase>(path);
// add the loaded asset to the "parts" list
m_ListProperty.arraySize += 1;
var elementProperty = m_ListProperty.GetArrayElementAtIndex(m_ListProperty.arraySize - 1);
elementProperty.objectReferenceValue = loadedPart;
// finally make the modified SerializedProperties persistent in the actual "Whole" asset
m_ListProperty.serializedObject.ApplyModifiedProperties();
// [optional] "Ping" the created asset => get highlighted in the Assets folder
EditorGUIUtility.PingObject(loadedPart);
// Close the popup window
Close();
}
}
}
}
}
#endif
}
And here a little demo how this would look like
For the demo I created the following types - all in their individual script files of course
public class PartBase : ScriptableObject { }
public class ExamplePart : PartBase { }
namespace NamespaceA
{
public class PartA : PartBase { }
}
namespace NamespaceA
{
public class PartAExtended : PartA { }
}
namespace NamespaceB
{
public class PartB : PartBase { }
}
namespace NamespaceB
{
public class PartBExtended : PartB { }
}

Inject new string into Fragment/ViewModel from MainActivity class

Working with the android studio Bottom Navigation activity template, want my HomeFragment / HomeViewModel page text to change each time the Home is accessed from the bottom navigation. When I click it the first time the text it displays should be different from the text displayed the 2nd time.
I'm able to create a variable that holds the text output, and I've created a function that should be able to update that variable but I'm unable to call it up within the MainActivity class. Any ideas?
MainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navView: BottomNavigationView = findViewById(R.id.nav_view)
val navController = findNavController(R.id.nav_host_fragment)
val appBarConfiguration = AppBarConfiguration(setOf(
R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications))
navView.setupWithNavController(navController)
}
HomeFragment
class HomeFragment : Fragment() {
private lateinit var homeViewModel: HomeViewModel
var chosenOutput:String = "primary output";
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
//using update here crashes the app
homeViewModel =
ViewModelProvider(this).get(HomeViewModel::class.java)
val root = inflater.inflate(R.layout.fragment_home, container, false)
val textView: TextView = root.findViewById(R.id.text_home)
homeViewModel.text.observe(viewLifecycleOwner, Observer {
textView.text = chosenOutput; //changing 'chosenOutput' to 'it' means that text update must take place within HomeViewModel instead of fragment.
})
return root
}
fun update(result: String)
{
chosenOutput = result;
}
}
HomeViewModel
class HomeViewModel : ViewModel() {
var temp:String = "demo"
//constructor()
private val _text = MutableLiveData<String>().apply {
value = temp;
}
fun update(result: String) //function may be obsolete if text change can be done in fragment.
{
temp = result;
}
val text: LiveData<String> = _text
}
What is the correct way to inject a string into either the Fragment or the ViewModel from the MainActivity class I've given?
You could change your approach and store your chosenOutput variable in the MainActivity
and update it everytime you select the HomeFragment
class MainActivity : AppCompatActivity()
{
var chosenOutput = "primary output"
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navView: BottomNavigationView = findViewById(R.id.nav_view)
val navController = findNavController(R.id.nav_host_fragment)
val appBarConfiguration = AppBarConfiguration(setOf(
R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications))
navView.apply {
setupWithNavController(navController)
setOnNavigationItemSelectedListener { menuItem ->
when (menuItem.itemId) {
R.id.navigation_home -> {
chosenOutput = "new value" // <-- Update chosen output here
// Add this method to call the `onResume` method in the fragment
showFragment(HomeFragment.newInstance())
}
else { }
}
true
}
}
private fun showFragment(fragment: Fragment) {
supportFragmentManager.beginTransaction()
.replace(R.id.nav_host_fragment, fragment)
.addToBackStack(null)
.commit()
}
}
Then you can retrieve the updated string in your Fragment with this code.
Note that I added the companion object to avoid the creation of multiple instances of the Fragment.
class HomeFragment : Fragment()
{
companion object
{
fun newInstance() : HomeFragment
{
return HomeFragment()
}
}
override fun onResume() {
super.onResume()
val newChosenOutput = (activity as MainActivity).chosenOutput
}
}

NSTextField with suggestions shown in dropdown

I am trying to implement autocomplete with an NSTextField where the user will type in some string and suggestions will be fetched from an API to be displayed under the text field. Optionally display a progress indicator inside the text field. So far I have designed the UI in Xcode IB and hooked the event to get the text change event.
public class UserTextFieldDelegate: NSTextFieldDelegate
{
public NSTextField Username { get; set; }
public UserTextFieldDelegate()
{
}
public UserTextFieldDelegate(NSTextField username)
{
this.Username = username;
}
public override void Changed(NSNotification notification)
{
Console.WriteLine(Username.StringValue);
}
}
The API will return a list of objects which I need to bind with the datasource of autocomplete list.
How do I achieve this in Xamarin.Mac?
In the NSTextField.Changed, save the NSTextView from the NSNotification parameter and call your Rest API:
NSString NSFieldEditor = new NSString("NSFieldEditor");
NSTextView editor;
[Export("controlTextDidChange:")]
public void Changed(NSNotification notification)
{
editor = editor ?? notification.UserInfo.ObjectForKey(NSFieldEditor) as NSTextView;
SomeRestCall(nsTextField.StringValue);
}
Now with your Rest method, call the actual Rest api via a background queue and save/buffer the completion words returned in a string array and then call NSTextView.Complete on the NSTextView instance variable that you saved from the Changed method:
string[] completionWords = { };
void SomeRestCall(string search)
{
if (editor != null)
{
DispatchQueue.GetGlobalQueue(DispatchQueuePriority.Background).DispatchAsync(() =>
{
if (string.IsNullOrWhiteSpace(search))
completionWords = new string[] { };
else
// Fake a REST call...
completionWords = (new string[] { "sushi", "stack", "over", "flow" })
.Where((word) => word.StartsWith(search, StringComparison.CurrentCulture)).ToArray();
if (editor != null)
DispatchQueue.MainQueue.DispatchAsync(() => { editor?.Complete(null); });
});
}
}
In your implementation of INSTextFieldDelegate add the GetCompletions protocol and return completion words that you saved in the last step:
[Export("control:textView:completions:forPartialWordRange:indexOfSelectedItem:")]
public string[] GetCompletions(NSControl control, NSTextView textView, string[] words, NSRange charRange, ref nint index)
{
requestor = null;
return completionWords;
`}

Implementing Xamarin Forms context actions

I am trying to implement context actions on my list in Xamarin Forms but can't get it to work.
I am not using XAML, but instead creating my layout in code.
I am trying to follow the steps in https://developer.xamarin.com/guides/xamarin-forms/user-interface/listview/interactivity/#Context_Actions and I want to push a new page when "Edit" is clicked.
I cleaned up my code and removed my feeble attempts to make things work.
So this is my custom list cell:
public class PickerListCell : TextCell
{
public PickerListCell ()
{
var moreAction = new MenuItem { Text = App.Translate ("Edit") };
moreAction.SetBinding (MenuItem.CommandParameterProperty, new Binding ("."));
moreAction.Clicked += async (sender, e) => {
var mi = ((MenuItem)sender);
var option = (PickerListPage.OptionListItem)mi.CommandParameter;
var recId = new Guid (option.Value);
// This is where I want to call a method declared in my page to be able to push a page to the Navigation stack
};
ContextActions.Add (moreAction);
}
}
And here is my model:
public class OptionListItem
{
public string Caption { get; set; }
public string Value { get; set; }
}
And this is the page:
public class PickerPage : ContentPage
{
ListView listView { get; set; }
public PickerPage (OptionListItem [] items)
{
listView = new ListView () ;
Content = new StackLayout {
Children = { listView }
};
var cell = new DataTemplate (typeof (PickerListCell));
cell.SetBinding (PickerListCell.TextProperty, "Caption");
cell.SetBinding (PickerListCell.CommandParameterProperty, "Value");
listView.ItemTemplate = cell;
listView.ItemsSource = items;
}
// This is the method I want to activate when the context action is called
void OnEditAction (object sender, EventArgs e)
{
var cell = (sender as Xamarin.Forms.MenuItem).BindingContext as PickerListCell;
await Navigation.PushAsync (new RecordEditPage (recId), true);
}
}
As you can see by my comments in the code, I have indicated where I believe things are missing.
Please assist guys!
Thanks!
Probably is too late for you, but can help others. The way i've found to do this is passing the instance of page on creation the ViewCell.
public class PickerListCell : TextCell
{
public PickerListCell (PickerPage myPage)
{
var moreAction = new MenuItem { Text = App.Translate ("Edit") };
moreAction.SetBinding (MenuItem.CommandParameterProperty, new Binding ("."));
moreAction.Clicked += async (sender, e) => {
var mi = ((MenuItem)sender);
var option = (PickerListPage.OptionListItem)mi.CommandParameter;
var recId = new Guid (option.Value);
myPage.OnEditAction();
};
ContextActions.Add (moreAction);
}
}
So, in your page:
public class PickerPage : ContentPage
{
ListView listView { get; set; }
public PickerPage (OptionListItem [] items)
{
listView = new ListView () ;
Content = new StackLayout {
Children = { listView }
};
var cell = new DataTemplate(() => {return new PickerListCell(this); });
cell.SetBinding (PickerListCell.TextProperty, "Caption");
cell.SetBinding (PickerListCell.CommandParameterProperty, "Value");
listView.ItemTemplate = cell;
listView.ItemsSource = items;
}
void OnEditAction (object sender, EventArgs e)
{
var cell = (sender as Xamarin.Forms.MenuItem).BindingContext as PickerListCell;
await Navigation.PushAsync (new RecordEditPage (recId), true);
}
}
Ok, so with the help of some posts, specifically this one https://forums.xamarin.com/discussion/27881/best-practive-mvvm-navigation-when-command-is-not-available, I came to the following solution, though I'm not perfectly satisfied with the way it looks.
My custom cell now announces when a command is being executed using MessagingCenter:
public class PickerListCell : TextCell
{
public PickerListCell ()
{
var moreAction = new MenuItem { Text = App.Translate ("Edit") };
moreAction.SetBinding (MenuItem.CommandParameterProperty, new Binding ("."));
moreAction.Clicked += async (sender, e) => {
var mi = ((MenuItem)sender);
var option = (PickerListPage.OptionListItem)mi.CommandParameter;
var recId = new Guid (option.Value);
// HERE I send a request to open a new page. This looks a
// bit crappy with a magic string. It will be replaced with a constant or enum
MessagingCenter.Send<OptionListItem, Guid> (this, "PushPage", recId);
};
ContextActions.Add (moreAction);
}
}
And in my PickerPage constructor I added this subscription to the Messaging service:
MessagingCenter.Subscribe<OptionListItem, Guid> (this, "PushPage", (sender, recId) => {
Navigation.PushAsync (new RecordEditPage (recId), true);
});
All this works just find, but I'm not sure if this is the way it was intended to. I feel like the binding should be able to solve this without the Messaging Service, but I can't find out how to bind to a method on the page, only to a model, and I don't want to pollute my model with methods that have dependencies on XF.

ListsView Binding with auto update

I have a application im working on that uses the Jabber Libraries to connect to a jabber Server and receive contacts etc.
I have buit all login system and interface for the chat but now im working on how to Bind the data for the contacts to the ListView
I have a function that is called when a contact comes online such, See Below
//AppController.cs
public void XmppConnection_OnRosterItem(Object Sender, RosterItem RosterItem)
{
if (LoginWindow.ActiveForm.InvokeRequired)
{
LoginWindow.ActiveForm.BeginInvoke(
new XmppClientConnection.RosterHandler(XmppConnection_OnRosterItem),
new object[] { Sender, RosterItem}
);
return;
}
//UPDATE HERE
}
The idea is to have a class such as ContactList so that when the above function is called i can go ContactList.AddRoster(Roster);
What i need to know is how do I create a custom list class and then bind it to the the Form witch holds the ListView element
And if possible set an update interval to recompile the ListVeiw?
Hope you guys can help me
Edit:
If I could have 2 classes one for the individual contact and one to hold the collection like so:
Contact C = new Contact(Roster.Name,Roster.Jid,Roster.Group);
ContactList.Add(C);
This as well would be good.
You could create a Contact class the just create a List of Contacts
List<Contact> ContactList=new List<Contact>();
ContactList.Add(Roster);
How to Bind ListView to List
http://www.vistax64.com/avalon/615-how-bind-listview-list-mystruct.html
Not sure about the update interval though. Attach it to a certain event and check the time in between maybe? MouseMove (Performance Cost?)
Anyone else have any ideas?
EDIT:
class ContactList:List<Contact>
{
public ContactList()
{
}
}
You shouldn't need to add anything to this class
class Contact
{
public string _Name;
public string _Jid;
public string _Group;
public Contact()
{
_Name = "Test";
_Jid = "One";
_Group = "Two";
}
public Contact(string Name, string Jid, string Group)
{
_Name = Name;
_Jid = Jid;
_Group = Group;
}
public override string ToString()
{
return _Name+" "+_Jid+" "+_Group;
}
}
Overiding the ToString function allows you easier control over what is displayed in the listbox
public Form1()
{
InitializeComponent();
ContactList C = new ContactList();
C.Add(new Contact("Name","Jid","Group"));
C.Add(new Contact());
C.Add(new Contact("Test","2","Something"));
for (int i = 0; i < C.Count; i++)
{
listView1.Items.Add(C[i].ToString());
}
}
Let me know if this works for you.

Categories

Resources