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
Related
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
I have implemented Tabbedpage using ViewModel but my ViewModel constructor call 4 times because I create 4 tabs, I also used prism for ViewModel binding.
Below is a design file
<?xml version="1.0" encoding="UTF-8"?>
<TabbedPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
xmlns:material="clr-namespace:XF.Material.Forms.UI;assembly=XF.Material"
xmlns:ffimageloading="clr-namespace:FFImageLoading.Forms;assembly=FFImageLoading.Forms"
xmlns:ffTransformations="clr-namespace:FFImageLoading.Transformations;assembly=FFImageLoading.Transformations"
prism:ViewModelLocator.AutowireViewModel="True"
xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"
xmlns:extended="clr-namespace:Xamarin.Forms.Extended;assembly=Xamarin.Forms.Extended.InfiniteScrolling"
xmlns:customcontrols="clr-namespace:QuranicQuizzes.CustomControls"
xmlns:local="clr-namespace:QuranicQuizzes.Views" NavigationPage.HasNavigationBar="True"
x:Class="QuranicQuizzes.Views.DashboardPage">
<NavigationPage.TitleView>
<StackLayout Orientation="Horizontal" HorizontalOptions="FillAndExpand">
<Label Text="Dashboard" TextColor="White" HorizontalTextAlignment="Center" HorizontalOptions="CenterAndExpand" VerticalTextAlignment="Center" FontFamily="{StaticResource QuranFontBold}" FontSize="Medium" />
<StackLayout Orientation="Horizontal">
<material:MaterialMenuButton x:Name="Menus" ButtonType="Text" Image="list" TintColor="White" BackgroundColor="Transparent" CornerRadius="24" Choices="{Binding Actions}" MenuSelected="MaterialMenuButton_MenuSelected" />
</StackLayout>
</StackLayout>
</NavigationPage.TitleView>
<local:HomeTabPage/>
<local:QuizzesTabPage/>
<local:LiveGameTabPage/>
<local:AssignmentTabPage/>
</TabbedPage>
Below is my code
public partial class DashboardPage : TabbedPage
{
private DashboardPageViewModel vm;
public DashboardPage()
{
try
{
InitializeComponent();
vm = BindingContext as DashboardPageViewModel;
}
catch (Exception ex)
{
}
}
}
Below is my ViewModel
public class DashboardPageViewModel : ViewModelBase
{
INavigationService _navigationService;
IClientAPI _clientAPI;
Dashboards dashboard;
public DashboardPageViewModel(INavigationService navigationService, IClientAPI clientAPI) : base(navigationService)
{
_navigationService = navigationService;
_clientAPI = clientAPI;
if (CrossConnectivity.Current.IsConnected)
{
var StartDate = DateTime.Now.AddDays(-7).ToString("yyyy-MM-dd");
var Enddate = DateTime.Now.ToString("yyyy-MM-dd");
if (dashboard == null)
{
dashboard = new Dashboards();
getDashboardData(StartDate, Enddate);
}
}
}
}
I see what you're trying to do. You want to initialise your vm instance so that you can access you vm from your view.
Instead of doing this:
vm = BindingContext as DashboardPageViewModel;
what we can do is change the type of the existing BindingContext property by doing this:
public partial class DashboardPage
{
new DashboardPageViewModel BindingContext
{
get => (DashboardPageViewModel) base.BindingContext;
set => base.BindingContext = value;
}
public DashboardPage()
{
InitializeComponent();
}
}
now you can just access BindingContext.DoSomething because its type is now DashboardPageViewModel.
Now that's sorted out, your viewmodel should not be being called 4 times! Something is wrong here. Here is a checklist of things to do that may be causing the constructor being called 4 times as not a lot more info was provided.
Try removing <NavigationPage.TitleView> segment.
Make sure you are navigating to DashboardPage.
Make sure that each individual TabbedPage has it's own viewmodel.
Try removing prism:ViewModelLocator.AutowireViewModel="True"and manually adding the viewmodel to the TabbedPage.
Finally constructors should be able to run very fast and should only be used for assigning variables or instantiation or very quick operations. What you could maybe do is separate the code in your VM:
public class DashboardPageViewModel : ViewModelBase
{
IClientAPI _clientAPI;
Dashboards dashboard;
public DashboardPageViewModel(INavigationService navigationService, IClientAPI clientAPI) : base(navigationService)
{
_clientAPI = clientAPI;
}
public void Init()
{
if (CrossConnectivity.Current.IsConnected)
{
var StartDate = DateTime.Now.AddDays(-7).ToString("yyyy-MM-dd");
var Enddate = DateTime.Now.ToString("yyyy-MM-dd");
if (dashboard == null)
{
dashboard = new Dashboards();
getDashboardData(StartDate, Enddate);
}
}
}
}
and then in your view you could add this method:
protected override void OnBindingContextChanged()
{
base.OnBindingContextChanged();
if(BindingContext == null)
{
return;
}
BindingContext.Init();
}
I hope this really helps you.
NB: All this code was written on the fly and never compiled, there may be some errors.
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();
I use AutoCompleteBox in MVVM and i want to execute something only if the user click on the Item or if the user press Enter.
But now when I use the down\Up Key on the keyboard the selectedItem property changes...
My controls :
<Controls:AutoCompleteBox ItemsSource="{Binding IndicationDtos, Mode=TwoWay}"
Width="100" SelectedItem="{Binding IndicationSelected, Mode=TwoWay}"
ValueMemberPath="Diagnosis" Text="{Binding Criteria, Mode=TwoWay}" MinimumPopulateDelay="250"/>
What can I do to make the property "SelectedItem" is assigned only on Enter or click?
If you have any question...
thanks a lot
In your SelectedItem binding, you can use:
SelectedItem="{Binding IndicationSelected, Mode=TwoWay, UpdateSourceTrigger=LostFocus}"
That way selected item only changes when you focus on something else
I found solution i created new class.
Like this :
public class AutoCompleteBoxEx : AutoCompleteBox
{
public static readonly DependencyProperty SelectionBoxItemProperty =
DependencyProperty.Register(
"SelectionBoxItem",
typeof(object),
typeof(AutoCompleteBox),
new PropertyMetadata(OnSelectionBoxItemPropertyChanged));
public object SelectionBoxItem
{
get
{
return GetValue(SelectionBoxItemProperty);
}
set
{
SetValue(SelectionBoxItemProperty, value);
}
}
protected override void OnDropDownClosing(RoutedPropertyChangingEventArgs<bool> e)
{
base.OnDropDownClosing(e);
SelectionBoxItem = SelectionAdapter.SelectedItem;
}
private static void OnSelectionBoxItemPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
}
}
I was just reading Rx HOL NET. Upon finding (example uses Windows Forms):
var moves = Observable.FromEvent<MouseEventArgs>(frm, "MouseMove");
I wonder how can I instantiate and pass the reference to moves to ViewModel in some WPF MVVM setup? In my understanding it does make sense to try and filter this stream of data inside ViewModel.
Or, how to do something similar for keyboard input into TextBox? In this scenario you wouldn't, for example, attach some text masking behavior to a control in XAML but would, instead, let Observer in VM filter and validate keyboard input.
Am I completely off the track?
Here is an example of how you could implement the web service dictionary in a MVVM fashion. It has three parts:
The ObservablePropertyBacking class, a backing for properties (of T) that also implements IObservable
The MyViewModel class. It contains a property CurrentText which uses an ObservablePropertyBacking as backing storage. It also observes the value of this property and uses it to call the dictionary web service.
The MainView.xaml which contains a TextBox. Its Text property is two-way bound to the CurrentText property on the view model.
MyViewModel.cs:
class MyViewModel: INotifyPropertyChanged
{
#region INotifyPropertyChanged implementation
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string p)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(p));
}
#endregion
public MyViewModel()
{
SetupProperties();
}
#region CurrentText
/* We use a special class for backing of the CurrentText property. This object
* holds the value of the property and also dispatches each change in an observable
* sequence, i.e. it implements IObservable<T>.
*/
private ObservablePropertyBacking<string> _textInput;
public string CurrentText
{
get { return _textInput.Value; }
set
{
if (value == _textInput.Value) { return; }
_textInput.Value = value;
RaisePropertyChanged("CurrentText");
}
}
#endregion
/* Create property backing storage and subscribe UpdateDictionary to the observable
* sequence. Since UpdateDictionary calls a web service, we throttle the sequence.
*/
private void SetupProperties()
{
_textInput = new ObservablePropertyBacking<string>();
_textInput.Throttle(TimeSpan.FromSeconds(1)).Subscribe(UpdateDictionary);
}
private void UpdateDictionary(string text)
{
Debug.WriteLine(text);
}
}
ObservablePropertyBacking.cs:
public class ObservablePropertyBacking<T> : IObservable<T>
{
private Subject<T> _innerObservable = new Subject<T>();
private T _value;
public T Value
{
get { return _value; }
set
{
_value = value;
_innerObservable.OnNext(value);
}
}
public IDisposable Subscribe(IObserver<T> observer)
{
return _innerObservable
.DistinctUntilChanged()
.AsObservable()
.Subscribe(observer);
}
}
MainPage.xaml:
<Window
x:Class="RxMvvm_3435956.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<TextBox
Text="{Binding CurrentText, UpdateSourceTrigger=PropertyChanged}" />
</Grid>
</Window>
This might help: Reactive Extensions (Rx) + MVVM = ?
The easiest way of doing the keyboard sample would be to two-way bind the text to a property of the ViewModel. The Text setter could then write to a private Subject that the rest of your code uses as a basis of IObservable<string>. From there, you can complete the HOL sample.
Mouse movements are generally considered too "view" to put in the ViewModel, but if the logic that came off it was complex enough, you could have it execute an ICommand or perhaps put the logic into a behavior. If it were an ICommand, you could have the command have a WhenExecuted IObservable property that you could pick up in your ViewModel.`