Xamarin.Forms: Is it possible to dynamically add rows to a grid without the ViewModel calling into the VIew? - mvvm

On a page where I am displaying a receipt to a user, I have a section which lists the subtotal, any taxes, and the total. Since the taxes applicable vary by region and based on the products being sold, the number of rows in this section will vary. For example, here are 3 separate orders, one with GST and PST, one with just GST, and one with neither:
I've accomplished this by putting just the SubTotal in the grid in XAML, and adding the rest in the code-behind in a method I call from the ViewModel. However, I'd really like to avoid doing it this way, so I'm wondering if there is an approach to accomplishing this which doesn't require having the ViewModel know about the View.
A ListView is not suitable here for a number of reasons:
These controls are inside of a ScrollView, and having a ListView inside of a ScrollView causes all sorts of weird problems.
I would like to keep the columns as narrow as their widest element. This is possible with a Grid, but a ListView would take up the entire width of its parent no matter what.
I neither need nor want for my rows to be selectable
So is there a way I can do this without the ViewModel knowing about the View and without resorting to using a ListView?

One way to encapsulate the functionality you require so that the view and the view model are not coupled is by creating a user control.
I created a new user control called TotalsGridControl. Here is the XAML
<?xml version="1.0" encoding="UTF-8"?>
<Grid xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="ScratchPad.UserControls.TotalsGridControl"
x:Name="TotalsGrid">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
</Grid>
And here is the code behind.
public partial class TotalsGridControl : Grid
{
public TotalsGridControl()
{
InitializeComponent();
}
public static readonly BindableProperty TotalsProperty =
BindableProperty.Create(nameof(Totals), typeof(List<TotalItem>), typeof(TotalsGridControl), null,
BindingMode.OneWay, null, OnTotalsChanged);
private static void OnTotalsChanged(BindableObject bindable, object oldvalue, object newvalue)
{
var control = (TotalsGridControl)bindable;
if (control != null)
{
if (newvalue is List<TotalItem> totals)
{
var rowNumber = -1;
double grandTotal = 0;
foreach (var totalItem in totals)
{
grandTotal += totalItem.Value;
var descLabel = new Label {Text = totalItem.Description};
var valueLabel = new Label { Text = totalItem.Value.ToString("c") };
rowNumber++;
control.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto});
control.Children.Add(descLabel, 0, rowNumber);
control.Children.Add(valueLabel, 1, rowNumber);
}
var grandTotalDescLabel = new Label { Text = "Total" };
var grandTotalValueLabel = new Label { Text = grandTotal.ToString("c") };
rowNumber++;
control.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
control.Children.Add(grandTotalDescLabel, 0, rowNumber);
control.Children.Add(grandTotalValueLabel, 1, rowNumber);
}
}
}
public List<TotalItem> Totals
{
get => (List<TotalItem>)GetValue(TotalsProperty);
set => SetValue(TotalsProperty, value);
}
}
I used a bindable property to allow a list of TotalItem to be bound to the user control.
Here is the data in the view model
public List<TotalItem> Totals { get; set; }
Totals = new List<TotalItem>
{
new TotalItem {Description = "SubTotal", Value = 99.91},
new TotalItem {Description = "GST", Value = 5.0},
new TotalItem {Description = "PST", Value = 4.9}
};
and here is the XAML in the page
<userControls:TotalsGridControl Totals="{Binding Totals}"/>
And the output

Before:
public class ReceiptPageModel : PageModelBase
{
private Receipt _receipt;
public Receipt Receipt
{
get => _receipt;
private set => Set(ref _receipt, value);
}
public override void Init(object initData)
{
Receipt = (Receipt) initData;
((ReceiptPage) CurrentPage).AddTaxes(Receipt);
}
}
After:
public class ReceiptPageModel : PageModelBase
{
private Receipt _receipt;
public Receipt Receipt
{
get => _receipt;
private set => Set(ref _receipt, value);
}
public override void Init(object initData)
{
Receipt = (Receipt) initData;
}
}
public partial class ReceiptPage : FreshBaseContentPage
{
public ReceiptPage()
{
InitializeComponent();
BindingContextChanged += HandlePageModelAdded;
}
private void HandlePageModelAdded(object sender, EventArgs e)
{
var pageModel = (ReceiptPageModel)BindingContext;
if (pageModel.Receipt != null)
{
AddTaxes(pageModel.Receipt);
}
else
{
pageModel.PropertyChanged += (s, args) =>
{
if (args.PropertyName == nameof(pageModel.Receipt))
AddTaxes(pageModel.Receipt);
};
}
}
private void AddTaxes(Receipt receipt)
{
...
}
}

Related

Make focus go to next entry in a collection view

I have an application in .Net Maui that uses a collection view with an entry field and after the collection view one static entry field. If you are currently focused on the first entry in the collection view and hit tab or enter it will not navigate to the next entry in the collection view and focus on the static entry field. I need to find the best way to have the entry focus on the next entry in the collection view on complete.
I have tried changing the return type of the collection view entry field to Next and also tried the community toolkit SetFocusOnEntryCompletedBehavior function and both result in the same behavior of not navigating to the next entry from the collection view. Very similar to this issue that doesnt seem to be resolved. MAUI - CollectionView jump / focus to next entry
I found a workaround for you. You could try the following code:
Step1 Create a custom control , let's call it MyEntry (MyEntry.cs) which subclass Entry:
In this control we attach a BindableProperty IsExpectedToFocusProperty which we used it to judge whether it is goning to be focused. We also registered a new method OnIsExpectedToFocus to detect propertyChanged for our control. For info about BindableProperty, you could refer to Bindable properties.
MyEntry.cs,
public class MyEntry : Entry
{
public static readonly BindableProperty IsExpectedToFocusProperty = BindableProperty.Create("IsExpectedToFocus", typeof(bool), typeof(MyEntry), false, propertyChanged:OnIsExpectedToFocus);
public bool IsExpectedToFocus
{
get => (bool)GetValue(IsExpectedToFocusProperty);
set => SetValue(IsExpectedToFocusProperty, value);
}
static void OnIsExpectedToFocus(BindableObject bindable, object oldValue, object newValue)
{
// Property changed implementation goes here
if ((bool)newValue == true)
{
(bindable as Entry).Focus();
}
}
}
Step2 Consume custom control in CollectionView. We define the ReturnCommand and its parameter. we will bind them in the MainPageViewModel.
MainPage.xaml,
<CollectionView x:Name="mycoll" ItemsSource="{Binding ItemCollection}">
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout>
<local:MyEntry x:Name="myentry" Focused="myentry_Focused"
IsExpectedToFocus="{Binding IsExpectedToFocus}"
Text="{Binding Title,Mode=TwoWay}" TextColor="Black"
ReturnCommand="{Binding Source={RelativeSource AncestorType={x:Type local:MainPageViewModel}}, Path=ReturnCommand}"
ReturnCommandParameter="{Binding .}"/>
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
In .cs file:
void myentry_Focused(System.Object sender, Microsoft.Maui.Controls.FocusEventArgs e)
{
var entry = sender as Entry;
foreach (var item in viewModel.ItemCollection)
{
if (entry.BindingContext != item)
{
item.IsExpectedToFocus = false;
}
}
}
Step3 Design our MainPageViewModel. I define an ObservableCollection which ItemSource will bind to. And add three items just for test.
Then I think the most important part is to design the Command. Let me explain it briefly. When we press the entry of an Entry, we fire the ReturnCommand and get current Item through ReturnCommandParameter. We get the index of current Item in ItemCollection. So the next entry which needs to be focused corresponds to the index+1 Item. Then we changed the IsExpectedToFocus of the next entry and fire the OnIsExpectedToFocus method which set the entry be focused. Done!
MainPageViewModel.cs
public class MainPageViewModel
{
public ObservableCollection<Item> ItemCollection { get; set; } = new ObservableCollection<Item>();
public Command ReturnCommand
{
get
{
return new Command<Item>((e) =>
{
e.IsExpectedToFocus = false;
int index = ItemCollection.IndexOf(e); // get the current index
if (index != -1)
{
int nextIndex;
// if last entry, next index is 0, else index +1
if (index < (ItemCollection.Count() - 1))
{
nextIndex = index + 1;
ItemCollection[nextIndex].IsExpectedToFocus = true;
}
else if(index == (ItemCollection.Count() - 1))
{
nextIndex = 0;
ItemCollection[nextIndex].IsExpectedToFocus = true;
}
}
});
}
}
public MainPageViewModel()
{
//add three item for test
ItemCollection.Add(
new Item
{
Title = "12345",
IsExpectedToFocus = false
}) ;
ItemCollection.Add(
new Item
{
Title = "23456",
IsExpectedToFocus = false
});
ItemCollection.Add(
new Item
{
Title = "34567",
IsExpectedToFocus = false
});
}
}
Also, this is Item.cs, should implement INotifyPropertyChanged
public class Item : INotifyPropertyChanged
{
public string title;
public bool isExpectedToFocus;
public string Title
{
get
{
return title;
}
set
{
title = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Title)));
}
}
public bool IsExpectedToFocus
{
get
{
return isExpectedToFocus;
}
set
{
isExpectedToFocus = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsExpectedToFocus)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
Hope it works for you.

set property of new instance

thank you for helping.
First, I created a form with a (user defined) property.
as see below
public partial class nfrmtableitem : Form
{
private DataRow _datarow;
public DataRow U_Table_Row { get { return _datarow; } set { _datarow = value; } }
public nfrmtableitem()
{
InitializeComponent();
}
}
And I create second form with property as type of Form.
as see below
public partial class nftableshow : Form
{
private DataTable _datatable;
public DataTable U_DataTable { get { return _datatable; } set { _datatable = value; } }
private Form _inputform1;
public Form U_DGV_InputForm1 { get { return _inputform1; } set { _inputform1 = value; } }
}
when call it:
any where
nftableshow newfrmtableshow = new nftableshow()
{
Name = "newfrmtableshow",
Text = "Show the table",
MdiParent = this,
U_DGV_InputForm1 = new nfrmtableitem(),
};
newfrmtableshow.Show();
But I can not use the first form property in second form.
and the property is not in instance.
//the button in second form
private void button1_Click_Click(object sender, EventArgs e)
{
Form f1 = _inputform1 as Form;
/*
* {
* U_Table_Row = db.maindataset.Tables["customer"].NewRow(),
* };
*/
f1.Show();
}
Question:
How can I use the First form with specific (user defined) property in second form.
Regards
You should probably use dot notation to access the property of the first form. Try using
//the button in second form
private void button1_Click_Click(object sender, EventArgs e)
{
Form f1 = _inputform1 as Form;
{
f1.U_Table_Row = db.maindataset.Tables["customer"].NewRow(),
};
f1.Show();
}

The type 'StackedColumnSeries' does not support direct content [duplicate]

I am trying to use WinRTXamlToolkit.Controls.DataVisualization.UWP
trying to draw any of the stacked charts like this:
But only this comes out:
Please help me, I have to use stacked series but the framework doesn't act as it should be..
Since I don't know how you define the code behind, I just provide the sample code as follows which can create a StackedLineSeries chart successfully.
XAML Code
<Page
x:Class="CStackLineChat.MainPage"
...
xmlns:charting="using:WinRTXamlToolkit.Controls.DataVisualization.Charting"
mc:Ignorable="d">
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" Padding="50" >
<charting:Chart x:Name="MyChart" Title="Stacked column Chart">
<charting:StackedLineSeries>
<charting:StackedLineSeries.SeriesDefinitions>
<charting:SeriesDefinition
DependentValuePath="Amount"
IndependentValuePath="Name"
IsTapEnabled="True"
Title="Doodad" />
<charting:SeriesDefinition
Title="Stan2"
DependentValuePath="Amount"
IndependentValuePath="Name"/>
</charting:StackedLineSeries.SeriesDefinitions>
</charting:StackedLineSeries>
</charting:Chart>
</Grid>
</Page>
Code behind
public sealed partial class MainPage : Page
{
private Random _random = new Random();
List<NameValueItem> Records = new List<NameValueItem>();
List<NameValueItem> Records2 = new List<NameValueItem>();
public MainPage()
{
this.InitializeComponent();
for (int i = 0; i < 5; i++)
{
Records.Add(new NameValueItem { Name = "Name" + i, Amount = _random.Next(10, 100) });
Records2.Add(new NameValueItem { Name = "Name" + i, Amount = _random.Next(10, 100) });
}
this.RunIfSelected(this.MyChart, () => ((StackedLineSeries)this.MyChart.Series[0]).SeriesDefinitions[0].ItemsSource = Records);
this.RunIfSelected(this.MyChart, () => ((StackedLineSeries)this.MyChart.Series[0]).SeriesDefinitions[1].ItemsSource = Records2);
}
private void RunIfSelected(UIElement element, Action action)
{
action.Invoke();
}
}
public class NameValueItem
{
public string Name { get; set; }
public int Amount { get; set; }
}
And the result
Additionally, by testing on my side, it seems like DependentValuePath and IndependentValuePath properties can not directly binding in your scenario. The best way to use this package is to follow the official sample. Here is the chart sample.

UWP WinRTXamlToolkit: chart-labels with units

For an UWP-app I'm displaying a chart with height data and it currently looks like this:
I'd like to have units for the y-values like 100 m, but I can only manage to get the value itself.
I am able to put a static unit behind that value by "StringFormat" in the "AxisLabelStyle" like this
<Setter Property="StringFormat" Value="{}{0:0 m}" />
but unfortunately I need a dynamic unit (e.g. meters or feet).
Am I missing anything? Ideas?
As we've discussed, this style is set by user. So I just use a ComboBox to select the style for test.
Here is my code:
<Charting:Chart x:Name="AreaChart" Title="Area Chart" Margin="0,0">
<Charting:AreaSeries x:Name="areaseries" IndependentValuePath="Value" DependentValuePath="Number" IsSelectionEnabled="True" />
</Charting:Chart>
<ComboBox x:Name="comboBox" VerticalAlignment="Bottom" SelectionChanged="comboBox_SelectionChanged">
<ComboBoxItem>Meters</ComboBoxItem>
<ComboBoxItem>Feet</ComboBoxItem>
</ComboBox>
code behind is just for testing, I didn't tried to rebuild your chart in your picture:
public MainPage()
{
this.InitializeComponent();
this.Loaded += MainPage_Loaded;
}
private void MainPage_Loaded(object sender, RoutedEventArgs e)
{
LoadChartContents();
}
private void LoadChartContents()
{
Random rand = new Random();
List<ChartTest> testitem = new List<ChartTest>();
for (int i = 0; i < 30; i++)
{
testitem.Add(new ChartTest() { Value = i, Number = rand.Next(0, 100) });
}
(AreaChart.Series[0] as AreaSeries).ItemsSource = testitem;
}
private void comboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
areaseries.IndependentAxis = new LinearAxis { Orientation = AxisOrientation.X };
var axis = (LinearAxis)areaseries.IndependentAxis;
var item = comboBox.SelectedItem as ComboBoxItem;
if ((string)item.Content == "Meters")
{
var labelstyle = new Style(typeof(AxisLabel));
labelstyle.Setters.Add(new Setter(AxisLabel.StringFormatProperty, "{0:0 m}"));
axis.AxisLabelStyle = labelstyle;
}
else
{
var labelstyle = new Style(typeof(AxisLabel));
labelstyle.Setters.Add(new Setter(AxisLabel.StringFormatProperty, "{0:0 feet}"));
axis.AxisLabelStyle = labelstyle;
}
}
And my is ChartTest class like this:
public class ChartTest
{
public int Value { get; set; }
public int Number { get; set; }
}
The key point here is dynamic adding AxisLabelStyle to AreaSeries in the SelectionChanged event of the ComboBox.

Create a BindableProperty depending on multiple properties

I'm trying to bind a UI element to different model properties A, B and AB. The first two properties A and B are controlled by two sliders. The third property AB is the sum of A and B. For each of the three properties there is a label displaying its value.
Now if I move one of the sliders, the corresponding label updates its Text. But the label for the combined property AB is not updated. Probably no "property changed" event is fired, since there is no setter for AB.
Is there any possibility for binding to such an "aggregated" property?
Here is the bindable object containing the properties A, B and AB:
public class Settings: BindableObject
{
public static readonly BindableProperty AProperty = BindableProperty.Create<Settings, double>(p => p.A, 0);
public static readonly BindableProperty BProperty = BindableProperty.Create<Settings, double>(p => p.B, 0);
public static readonly BindableProperty ABProperty = BindableProperty.Create<Settings, double>(p => p.AB, 0);
public double A {
get{ return (double)GetValue(AProperty); }
set{ SetValue(AProperty, (double)value); }
}
public double B {
get{ return (double)GetValue(BProperty); }
set{ SetValue(BProperty, (double)value); }
}
public double AB {
get{ return A + B; }
}
}
And here is the page containing both sliders and the three labels:
public class App : Application
{
public App()
{
var settings = new Settings();
var sliderA = new Slider();
sliderA.ValueChanged += (sender, e) => settings.A = e.NewValue;
var sliderB = new Slider();
sliderB.ValueChanged += (sender, e) => settings.B = e.NewValue;
var labelA = new Label{ BindingContext = settings };
labelA.SetBinding(Label.TextProperty, "A");
var labelB = new Label{ BindingContext = settings };
labelB.SetBinding(Label.TextProperty, "B");
var labelAB = new Label{ BindingContext = settings };
labelAB.SetBinding(Label.TextProperty, "AB");
MainPage = new ContentPage {
Content = new StackLayout {
VerticalOptions = LayoutOptions.Center,
Children = { sliderA, sliderB, labelA, labelB, labelAB },
},
};
}
}
This is what the running application looks like on iOS:
The last label should display the sum of the first two numbers.
Edit:
I wonder why I can't write
public static readonly BindableProperty ABProperty =
BindableProperty.Create<Settings, double>(p => p.A + p.B, 0);
But this yields the run-time error "System.TypeInitializationException: An exception was thrown by the type initializer for AggregatedBindablePropertyMnml.Settings ---> System.Exception: getter must be a MemberExpression"
Based on the suggestion from Taekahn (updating AB within the setters of A and B) I came up with the following solution.
By overriding the OnPropertyChanged method and setting the ABProperty, the bound label text is updated as well. In contrast to modifying each setter individually, this way we only need to modify the Settings class at one place.
protected override void OnPropertyChanged(string propertyName = null)
{
base.OnPropertyChanged(propertyName);
SetValue(ABProperty, A + B);
}
Now both sliders impact the third label: