I am working on a Windows Phone 7 Application using Local SQLite Database and I'm having an issue with the rendering time of pages that use DataBinding.
Currently it takes 60-70ms to retrieve the data from the database. Then it takes about 3100ms to render the data retrieved using a ListBox with DataBinding.
Here you can see the DataTemplate of the ListBox:
<DataTemplate x:Key="ListBoxItemTemplate">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="68" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock x:Name="TimeColumn"
Text="{Binding TimeSpan}" Grid.Column="0" Grid.Row="0"
Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center" />
<TextBlock Text="{Binding Stop.StopName}" Grid.Column="1" Grid.Row="0"
Margin="15,0,0,0" TextWrapping="NoWrap" Foreground="Black"
HorizontalAlignment="Left" VerticalAlignment="Center" />
</Grid>
</DataTemplate>
Comment: I have tried it using Canvas instead of Grid too, same result.
Then, the database loads data into a CSList (using ViciCoolStorage) and that gets Binded to the ListBox:
StationList.ItemsSource = App.RouteViewModel.RouteStops;
Comment: I have tried to add the elements of the CSList to an ObservableCollection and bind that to the interface but didn't seem to change anything.
Question:
Am I doing something wrong that results in a huge load time - even if just loading 10 elements -, or this is normal? Do you have any recommendations to get a better performance with DataBinding?
Thank you for your answers in advance!
Corresponding Code Parts:
RouteViewModel.cs
private Route rRoute;
public Route Route
{
get
{
if (rRoute != null)
{
return rRoute;
}
else
{
return new Route();
}
}
}
public void LoadRoute(string index)
{
try
{
if (rRoute.RouteId != index)
{
RouteLoaded = false;
StationsLoaded = false;
TimetableLoaded = false;
}
}
catch (Exception) { }
this.index = index;
if (!RouteLoaded)
{
NotifyPropertyChanging("Route");
rRoute = Route.ReadSafe(index);
RouteLoaded = true;
NotifyPropertyChanged("Route");
}
}
private CSList<RouteTime> rtLine;
public CSList<RouteTime> RouteStops
{
get
{
if (rtLine != null)
{
return rtLine;
}
else
{
return new CSList<RouteTime>();
}
}
}
public void LoadRouteStops()
{
LoadRoute(index);
if (!this.StationsLoaded)
{
NotifyPropertyChanging("RouteStops");
rtLine = rRoute.RouteTimes.FilteredBy("DirectionId = #DirectionId", "#DirectionId", this.direction).OrderedBy("TimeSpan");
NotifyPropertyChanged("RouteStops");
StationsLoaded = true;
}
}
RouteView.xaml.cs
private string index;
private bool visszaut = false;
public RouteView()
{
InitializeComponent();
Loaded += new System.Windows.RoutedEventHandler(RouteView_Loaded);
}
void RouteView_Loaded(object sender, System.Windows.RoutedEventArgs e)
{
DataContext = App.RouteViewModel;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
NavigationContext.QueryString.TryGetValue("index", out index);
App.RouteViewModel.LoadRoute(index);
App.RouteViewModel.Direction = Convert.ToInt32(visszaut);
App.RouteViewModel.LoadRouteStops();
StationList.ItemsSource = App.RouteViewModel.RouteStops;
}
RouteTime.cs - Class Implementation
[MapTo("RouteTimes")]
public class RouteTime : CSObject<RouteTime, int>
{
public int RouteTimeId
{
get
{
return (int)GetField("RouteTimeId");
}
set
{
SetField("RouteTimeId", value);
}
}
public int RouteId
{
get
{
return (int)GetField("RouteId");
}
set
{
SetField("RouteId", value);
}
}
public int StopId
{
get
{
return (int)GetField("StopId");
}
set
{
SetField("StopId", value);
}
}
public int TimeSpan
{
get
{
return (int)GetField("TimeSpan");
}
set
{
SetField("TimeSpan", value);
}
}
public Direction DirectionId
{
get
{
return (Direction)GetField("DirectionId");
}
set
{
SetField("DirectionId", value);
}
}
[OneToOne(LocalKey = "StopId", ForeignKey = "StopId")]
public Stop Stop
{
get
{
return (Stop)GetField("Stop");
}
set
{
SetField("Stop", value);
}
}
[ManyToOne(LocalKey = "RouteId", ForeignKey = "RouteId")]
public Route Route
{
get
{
return (Route)GetField("Route");
}
set
{
SetField("Route", value);
}
}
}
Okay so, in this scenario it seems the source of the slow rendering was the [OneToOne] connection between the RouteTime and Stop classes. If I bind to any of the variables that is stored in the linked class, the rendering takes a long time.
Fixed by
I have added a new partial class in the code where I need to show the results.
public partial class StopTimes
{
public int TimeSpan
{
get;
set;
}
public string StopName
{
get;
set;
}
}
Using an ad-hoc query in Vici CoolStorage, I've made my own query to request the data needed and viola, everything is there and rendering didn't take more then 1 second. Perhaps, during the rendering it requests the StopName field with an SQLQuery one by one?
Don't know, but thank you for your help anyway :)
DataBinding should not be the problem in this scenario - I never had any problems with it. It has something to do with your SQL DB for sure. I had lists of around 200 items, and it rendered fine under a reasonable time-slot of 100ms.
It either is your SQL implementation or you are using ViciCoolStorage wrong. Post your code.
Have you tried doing the filtering of your list outside of the LoadRouteStops() method? Maybe in the code behind instead of the viewModel? It seems like a lot of work to be done in between propertyChanging and propertyChanged notifications.
I'm confused: How do you do local SQLite on Windows Phone 7? Did you mean WP8 where this is supported?
In any case, consider using LongListSelector (built into WP8 - Toolkit in WP7), since it does a better job with virtualization when you have a lot of items to render.
The problem is mostly because of setting a lot of binding. Try to keep bindings minimal and try to do the hard work on the code behind. Such as;
"binding a list of string to xaml one by one"
versus
"by string.join method with just one binding to a text block"
I had
<TextBlock Text="{Binding Times[0]}"/>
<TextBlock Text="{Binding Times[1]}"/>
...
<TextBlock Text="{Binding Times[9]}"/>
And changed to
<TextBlock Text="{Binding TimesToLongString}"/>
This fixed the delay.
Used to load in 9 sec with 3 items.
Now it loads within 110ms with 10 items.
To check the time it takes to load the list, you can check out this post
Related
I want to create complicate form which will create structure of wizard, partial steps form, validation and submit. This structure have to use model attributes annotations to create one structure object over the model. So after reflection I have model and one other class with structure description. All properties within are strings with Fields which I have to pass on 'asp-for' tag helper. So part of the code is:
#foreach(var field in #group.Fields) {
<div class="col-12 col-md-6 col-lg-4">
<div class="form-group md-form md-outline">
<label asp-for="#field.Name" class="control-label"></label>
<input asp-for="#field.Name" class="form-control" />
<span asp-validation-for="#field.Name" class="text-danger"></span>
</div>
</div>
}
This is nor working because tag helper expect expression and generate wrong values which are not expected from me. The value in #field.Name is 'PostAddress.Street1'. If I replace all of "#field.Name" with "PostAddress.Street1" everything work properly how I expected.
It looks small issue but I'm trying many things and reading some theads in forums but didn't find the answer. What I tried:
Experiment 1
Tried to inherit InputTagHelper class from dotnet library and override property For but without success. It changed ModelExpression but no changes in interface. May be base class have some logic to skip this changed object or is not correct generated:
[HtmlAttributeName("asp-for")]
public new ModelExpression For
{
get
{
return base.For;
}
set
{
ModelExpression me = value;
if (value.Model != null)
{
var viewData = this.ViewContext.ViewData as ViewDataDictionary<AbnServiceModel>;
me = ModelExpressionProvider.CreateModelExpression<AbnServiceModel, string>(viewData, model => model.PostAddress.Street1);
}
base.For = me;
}
}
=================================================
2. Experiment 2
Try to get original implementation from .NET Core code and made some modification in code to fix the issue. But the code and dependencies with internal libraries were very complicated and I reject this idea.
Expiriment 3
Using HTML helpers
#Html.Label(#field.Name, "", new{ #class="control-label" })
#Html.Editor(#field.Name, new { htmlAttributes = new{ #class="form-control" } })
#Html.ValidationMessage(#field.Name,"",new { htmlAttributes = new{ #class="text-danger" } })
It render components correct into the browser but client side validation using jquery.validate.unobtrusive.js is not working. Not sure why.
Expiriment 4
Using HTML helpers:
#Html.LabelFor(m=>m.PostAddress.Street1, new{ #class="control-label" })
#Html.EditorFor(m=>m.PostAddress.Street1, new { htmlAttributes = new{ #class="form-control" } })
#Html.ValidationMessageFor(m=>m.PostAddress.Street1,"",new { htmlAttributes = new{ #class="text-danger" } })
The validation is working but class weren't applied well, may be my mistake. But other problem here is that I'm not using expression which is string which can get from model object. Also It doesn't catch all logic which is included in asp-for tag helper.
Experiment 5
Tried to create my own tag helper and using generator to create the content html. But this means that I have to implement all logic like helper in dotnet core to have all functionality which is same like Expiriment 2
So I didn't find good solution of this "simple" problem and lost some days to investigate and doing some code to resolve it. I'm surprised that no way to pass string variable with property name and it wouldn't work.
Can someone help me to fix this problem with real example? I didn't find the answer in all posts. I want to have all logic from asp-for tag helper but use variable to pass the expression. It cab be and tricky, just want to have some resolution to continue with my project.
Thank you
I resolved my issue.
Created one helper method:
public static class CommonHelperMethods
{
public static ModelExplorer GetModelExplorer(this ModelExplorer container, string field, IModelMetadataProvider modelMetadataProvider = null)
{
ModelExplorer result = container;
var fields = field.Split(".").ToList();
var match = Regex.Match(fields[0], #"(.+)\[(\d)+\]");
if (!match.Success)
{
fields.ForEach(x =>
{
result = result?.GetExplorerForProperty(x) ?? result;
});
}
else
{ //List have to create own Property browser
string proName = match.Groups[1].Value;
int idx = Convert.ToInt32(match.Groups[2].Value);
var model = ((IList)result?.GetExplorerForProperty(proName).Model)[idx];
var targetProperty = model.GetType().GetProperty(fields[1]);
var targetValueModel = targetProperty.GetValue(model);
var elementMetadata = modelMetadataProvider.GetMetadataForProperty(model.GetType(), fields[1]);
return new ModelExplorer(modelMetadataProvider, container, elementMetadata, targetValueModel);
}
return result;
}
}
And just override the tag helper class with this:
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.TagHelpers;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace GetTaxSolutions.Web.Infrastructure.TagHelpers
{
[HtmlTargetElement("input", Attributes = ForAttributeName, TagStructure = TagStructure.WithoutEndTag)]
public class InputTextGtTaxHelper : InputTagHelper
{
private const string ForAttributeName = "asp-for";
[HtmlAttributeName("not-exp")]
public bool NotExpression { get; set; } = false;
[HtmlAttributeName(ForAttributeName)]
public new ModelExpression For
{
get
{
return base.For;
}
set
{
ModelExpression me = value;
if (NotExpression)
{
var modelExplorertmp = value.ModelExplorer.Container.GetModelExplorer(value.Model.ToString(), ModelMetadataProvider);
var modelExplorer = new ModelExplorer(ModelMetadataProvider, value.ModelExplorer.Container, modelExplorertmp.Metadata, modelExplorertmp.Model);
me = new ModelExpression(value.Model.ToString(), modelExplorer);
}
base.For = me;
}
}
public IModelExpressionProvider ModelExpressionProvider { get; }
public IModelMetadataProvider ModelMetadataProvider { get; }
public IActionContextAccessor Accessor { get; }
public InputTextGtTaxHelper(
IHtmlGenerator generator,
IModelExpressionProvider modelExpressionProvider,
IModelMetadataProvider modelMetaDataProvider) : base(generator)
{
ModelExpressionProvider = modelExpressionProvider;
ModelMetadataProvider = modelMetaDataProvider;
}
}
}
Also should skip original class in tag helper registration:
#addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
#removeTagHelper Microsoft.AspNetCore.Mvc.TagHelpers.InputTagHelper, Microsoft.AspNetCore.Mvc.TagHelpers
#removeTagHelper Microsoft.AspNetCore.Mvc.TagHelpers.LabelTagHelper, Microsoft.AspNetCore.Mvc.TagHelpers
#removeTagHelper Microsoft.AspNetCore.Mvc.TagHelpers.ValidationMessageTagHelper, Microsoft.AspNetCore.Mvc.TagHelpers
#removeTagHelper Microsoft.AspNetCore.Mvc.TagHelpers.SelectTagHelper, Microsoft.AspNetCore.Mvc.TagHelpers
#addTagHelper GetTaxSolutions.Web.Infrastructure.TagHelpers.*, GetTaxSolutions.Web
And when use model in expression just have to pass attribute 'no-exp' on input elements. Otherwise will work like original tag helper.
<input not-exp="true" asp-for="#field.Name" class="form-control" />
Also you have to do same with label, select and other used tag helpers which you want to support this way of model passing.
<div class="form-gridcontrol">
<label>Notes</label>
#Html.CustomTextArea(m => m.Notes)
</div>
In ASP.NET MVC , I have created a custom textarea and inputing/displaying data from the database using a Model.Above is the code where you can see the Notes are getting assigned to #Html.CustomTextArea.
I have a situation where , I need to display a text "Not Applicable" if there is no value in "m.Notes"
How I should right the logic in the above code? Please guide.
There are multiple possible ways for this. One of the way is that you can populate in the controller action from where it is loaded like:
public ActionResult YourActionMethod()
{
............
............
if(String.IsNullOrEmpty(model.Notes))
model.Notes = "Not Applicable";
return View(model);
}
Another way can be to introdcue the backing field on your property and write in it's getter:
private String _notes;
public String Notes
{
get
{
return String.IsNullOrEmpty(_notes) ? "Not Applicable" : _notes;
}
set
{
_notes = value;
}
}
You can try this:
#if (Model.Notes != null)
{
#Html.CustomTextArea(m => m.Notes)
}
else
{
#Html.CustomTextArea( m => m.Notes, new { #Value = "Not Applicable"})
}
edit:this is not working with textarea
else
{
#Html.CustomTextArea(m => m.Notes, new {id="mytextarea"})
<script>
$("#mytextarea").text("Not Applicable")
</script>
}
I got a trick for you :)
How do you convert input value to title case in EditorFor? I know doing
#Html.EditorFor(model, new { htmlAttributes = new { #style = "text-transform:uppercase" } })
will only change the client side so I need to change it manually on server side.
I tried adding the class text-capitalize but seems no luck.
Thanks in advance.
Here are explanations to use either title case or sentence case for viewmodel's string properties which bound to EditorFor:
1) If you want to use title case, you can set it inside getter part with ToTitleCase method (change CurrentCulture to InvariantCulture depending on your requirements), as in example below:
private string _titleCase;
private System.Globalization.CultureInfo culture = System.Threading.Thread.CurrentThread.CurrentCulture;
public string TitleCaseProperty
{
get
{
if (string.IsNullOrEmpty(_titleCase))
{
return _value;
}
else
{
return culture.TextInfo.ToTitleCase(_titleCase.ToLower());
}
}
set
{
_titleCase = value;
}
}
View usage
#Html.EditorFor(model => model.TitleCaseProperty, ...)
2) If you want sentence case instead, use regular expression to find out sequences (referenced from this similar issue) and do similar way to getter part like above:
private string _sentenceCase;
private Regex rgx = new Regex(#"(^[a-z])|[?!.:,;]\s+(.)", RegexOptions.ExplicitCapture);
public string SentenceCaseProperty
{
get
{
if (string.IsNullOrEmpty(_sentenceCase))
{
return _value;
}
else
{
return rgx.Replace(_sentenceCase.ToLower(), s => s.Value.ToUpper());
}
}
set
{
_sentenceCase = value;
}
}
View usage
#Html.EditorFor(model => model.SentenceCaseProperty, ...)
Live example: .NET Fiddle Demo
I would recommend performing this conversion at the getter of this property using .ToUpper()
get {
if (string.IsNullOrEmpty(_value))
{
return _value;
}
return _value.ToUpper();
}
easier method
#Html.TextBoxFor(model.FieldName, new { #class = "uppercase" })
css:
.uppercase { text-transform:uppercase }
I've got the following snippet of Razor code, that exists in probably 15 different pages, that I'd like to reuse, if possible:
<div class="col-xs-12">
#if (#Model.Rating == 0)
{
<img src="/Images/Rating/NoRating.jpg" alt="" width="125">
}
else if (#Model.Rating == 1)
{
<img src="/Images/Rating/One.jpg" alt="" width="125">
}
else if (#Model.Rating == 2)
{
<img src="/Images/Rating/Two.jpg" alt="" width="125">
}
else if (#Model.Rating == 3)
{
<img src="/Images/Rating/Three.jpg" alt="" width="125">
}
else if (#Model.Rating == 4)
{
<img src="/Images/Rating/Four.jpg" alt="" width="125">
}
else if (#Model.Rating == 5)
{
<img src="/Images/Rating/Five.jpg" alt="" width="125">
}
</div>
What I would love to be able to do is to call a method and have the method return this code where I have it in my Razor .cshtml file. The method would also have to accept a parameter. In this case, the parameter would be a rating value of between 0 and 5. I would then replace all occurrences of #Model.Rating with the parameter value. Is it possible to do this? I'd rather not have to resort to a partial view if possible.
What I would love to be able to do is to call a method and have the method return this code where I have it in my Razor .cshtml file. The method would also have to accept a parameter. In this case, the parameter would be a rating value of between 0 and 5. I would then replace all occurrences of #Model.Rating with the parameter value. Is it possible to do this?
Solution 1:
You can create an extension method of HtmlHelper class like this :
public static class RatingExtensions
{
public static MvcHtmlString Rating(this HtmlHelper helper, short rating)
{
var imageSrc = "/Images/Rating/";
switch (rating)
{
case 0:
imageSrc += "NoRating.jpg";
break;
case 1:
imageSrc += "One.jpg";
break;
// And so on....
default:
throw new IndexOutOfRangeException(string.Format("The following rating: {0} is not expected.",
rating));
}
return new MvcHtmlString(String.Format("<img src='{0}' alt='' width='125' />", imageSrc));
}
}
In your view after importing the namespace of your extension method into the view, you call your extension method by writing this line:
#Html.Rating(Model.Rating)
Solution 2:
Just create a partial view and put it into the Shared sub-folder of your Views folder. Lets name it _Ratring.cshtml. The content of this file muste be the following (Notice #model directive which is in short type):
#model short
#{
var imageSrc = "/Images/Rating/";
switch (Model)
{
case 0:
imageSrc += "NoRating.jpg";
break;
case 1:
imageSrc += "One.jpg";
break;
// And so on....
default:
throw new IndexOutOfRangeException(string.Format("The following rating: {0} is not expected.",
Model));
}
}
<div class="col-xs-12">
<img src="#imageSrc" alt="" width="125">
</div>
You use this solution in your view by call Html.RenderPartial method liek this :
#{
Html.RenderPartial("_Rating", Model.Rating);
}
Solution 1 is better because you can move the extension method in its own assembly project and use it accross multiple projects.
Option 1
You may create a custom Html helper method. I would prefer to rename the image name from One.jpg to 1.jpg so that you do not need to write much code from the number passed into the string representation of that. You can simply relate your Model.Rating value to the image name as they directly match.
But if you still want to keep the image names as the string way, You may need to write a switch statement inside your method to convert the integer value to the string value (1 to One, 2 to Two etc..). The problem with this approach is, If you ever add a new rating like 12 or 13, you need to go to this file and update your switch statements again ! So i prefer the first approach of renaming the image names to match with the numeric representation of Model.Rating value
public static class MyCustomImageExtensions
{
public static MvcHtmlString RatingImage(this HtmlHelper helper, int imageId,
string alt,int width)
{
var path = "Images/Rating/NoRating.jpg";
if (imageId > 0)
{
path = string.Format("Images/Rating/{0}.jpg", imageId);
}
return new MvcHtmlString(string.Format("<img src='{0}' alt='{1}' width='{2}'>"
, path, alt, width));
}
}
You may call it in your razor view like
#Html.RatingImage(Model.Rating,"alt test",250)
You may add the Alternate text property and width to your model so that you do not need to hard code it in your main view.
Option 2
Since you are not doing much logic inside the helper method, you may simply use partial view where you will have the markup you want to use and pass the model properties to that.
I would change a lot, but it's really worth it. Create your own ModelMetadataProvider.
public class MyModelMetaDataProvider : DataAnnotationsModelMetadataProvider
{
public override ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, string propertyName)
{
var result = base.GetMetadataForProperty(modelAccessor, containerType, propertyName);
if (string.IsNullOrEmpty(result.TemplateHint)
&& typeof(Enum).IsAssignableFrom(result.ModelType))
{
result.TemplateHint = result.ModelType.ToString();
}
return result;
}
}
Register it in the global.asax
ModelMetadataProviders.Current = new MyModelMetaDataProvider();
Create an Enum:
public enum StarRating
{
NoRating = 0,
One = 1,
Two = 2,
Three = 3,
Four = 4,
Five = 5
}
Update your model
public class SomeMethodViewModel
{
public StarRating Rating { get; set; }
}
Create a Display template
/Views/Shared/DisplayTemplates/StarRating.cshtml
#model StarRating
<img src="/Images/Rating/#(Model.ToString()).jpg" alt="" width="125">
In your view:
#Html.DisplayFor(m => m.Rating)
I'm new to Mvc and I'm having an image that can have a correct or wrong image depending on the answer the user gives.
This is my current code:
#if (Model.IsCorrect)
{
<img src="#Url.Content(#"~/Content/images/Default_Correct.png")" alt="correct" />
}
else
{
<img src="#Url.Content(#"~/Content/images/Default_Wrong.png")" alt="wrong" />
}
This works perfectly but I think there must be a much cleaner/better way to do something like this.
If you are like me and hate polluting your views with spaghetti code you could write a custom helper:
public static class ImageExtensions
{
public static IHtmlString MyImage(this HtmlHelper htmlHelper, bool isCorrect)
{
var urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext);
var img = new TagBuilder("img");
if (isCorrect)
{
img.Attributes["alt"] = "correct";
img.Attributes["src"] = urlHelper.Content("~/Content/images/Default_Correct.png");
}
else
{
img.Attributes["alt"] = "wrong";
img.Attributes["src"] = urlHelper.Content("~/Content/images/Default_Wrong.png");
}
return MvcHtmlString.Create(img.ToString(TagRenderMode.SelfClosing));
}
}
and in your view simply:
#Html.MyImage(Model.IsCorrect)