I developed site using ASP.net & c# it support English language, but now i want to convert my site to
Multi language supported site. Any one please explain, how to do it? and what are the problems i face?
<div id="google_translate_element"></div>
<script type="text/javascript">
function googleTranslateElementInit() {
new google.translate.TranslateElement({pageLanguage: 'en'}, 'google_translate_element');
}
</script>
<script type="text/javascript" src="//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit"></script>
For c# backend:
Create a new language project in your solution, give it a name like ProjectName.Language. Add a folder to it called Resources, and in that folder, create Resources files (.resx) for each language you want to support.
Reference to your project: Right click on References -> Add Reference -> Prjects\Solutions.
Use namespace in a file: using ProjectName.Language;
Use it like: string someText = Resources.productGeneralErrorMessage;
Generally, you can load the labels and text in your View pages dynamically, with the label data stored in a local database. Depending on the number of expected page request you could use a lite weight database like SQLite.
Place buttons for the user to select a language.
Basic Method:
Install SQLite by NUGET package manager
add a using statement
using System.Data.SQLite;
Create a database named mydb.db
SQLiteConnection.CreateFile("mydb.db");
Create a connection method
public const string connectionString = #"Data Source=.\mydb.db;Version=3";
public static SQLiteConnection ConnectionFactory()
{ return new SQLiteConnection(connectionString); }
Create a label Table method:
public void CreateTable()
{
string createTableQuery=
#"CREATE TABLE IF NOT EXISTS [LabelLanguage] (
[id] INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
[labelName] Text NULL,
[englishValue] Text NULL,
[germanValue] Text Null
)";
using (IDbConnection connection = ConnectionFactory())
{ connection.Execute(createTableQuery);}
}
Populate database table with info
public void SaveQuery()
// a better approach would be to create a list of labels and values the use a for each to add them to database
//insert label 1 with values for house
{
string saveQuery = #""insert into LabelLanguage
(labelName, englishValue, germanValue) values
(label1, house, hous) "
using (IDbConnection connection = new SQLiteConnection(connectionString))
{
connection.Execute(saveQuery);
}
// insert label 2 for values of car
saveQuery = #""insert into LabelLanguage
(labelName, englishValue, germanValue) values
(label1, car, Wagen) "
using (IDbConnection connection = new SQLiteConnection(connectionString))
{
connection.Execute(saveQuery);
}
}
```
Create a class to hold label values
public class MyLabelValue
{
// should match database spellings
public string labelName{ get; set; }
public string englishValue{ get; set; }
public string germanValue{ get; set; }
}
add Dapper to the project [ maps classes ]
via nuget package manger
create a method to Get a list of labels and values from database
public List<MyLabelValue> GetLabelLanguageValues(string languageName )
{
using (IDbConnection connection = ConnectionFactory())
{
return connection.Query<MyLabelValue>($"SELECT labelName, "+ languageName +" FROM LabelLanguage").ToList();
}
}
Create a method to loop through each label on your form
'''
void LoopThroughLabels(string languageName )
{
Label mylabel;
list listOfLanguageValues = GetLabelLanguageValues(languageName );
foreach (Control con in this.Controls)
{
if (con.GetType() == typeof (Label)) //or any other logic
{
mylabel = (Label)con;
foreach (MyLabelValue languageValue in listOfLanguageValues )
{
if (mylabel.name.ToString() == languageValue.labelName.ToString()
{
```
//uses reflection so make sure the property GetProperty("languageName ") is how the column in database is spelt and the property of the class MyLabelValue
mylabel.Text= languageValue.GetType().GetProperty(languageName ).GetValue(languageValue, null); ;
}
}
}
}
}
Note: probably quite a few mistakes in there
and it relies on the exact naming of labels and also of language names
First of all if we are talking about Localization:
There is a way of doing that.
In your solution explorer, create a folder named: Resources
Go to your Startup.cs
In your ConfigureServices register your Localization Settings for Resources folder:
services.AddLocalization(options =>
{
// Store localization files under the Resources folder.
options.ResourcesPath = "Resources";
});
And also in ConfigureServices while you are registiring you MVC please add following:
services.AddMvc()
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
.SetCompatibilityVersion(Microsoft.AspNetCore.Mvc.CompatibilityVersion.Version_2_2);
services.Configure<RequestLocalizationOptions>(options =>
{
var supportedCultures = new List<CultureInfo>
{
new CultureInfo("en-US"), // => English option
new CultureInfo("it-it") // => Italian option
};
options.DefaultRequestCulture = new RequestCulture("en-US");
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
});
And now in your Cofigure method please add following:
var options = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();
app.UseRequestLocalization(options.Value);
Now please create a Controller named could be => LanguageController.cs
Please add the following:
//This will be your main Cookie Culture Info controller.
public class LanguageController : Controller
{
[HttpPost]
public IActionResult SetLanguage(string culture, string returnURL)
{
Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture))
);
return LocalRedirect(returnURL);
}
}
Now, you have to create .resx files under your Resources File:
Lets say you have a HomeController.cs and a public IActionResult Index() method,
So, for this page you have to create a .resx file like following:
->English
Views.Home.Index.en-US.resx
->Italian
Views.Home.Index.it-it.resx
These .resx files are easy to use, like an excell view and a xml format you have to give same keys but different values for each .resx file.
Example:
-> en-US.resx -> Key: Hello and Value: Hello
-> it-it.resx -> Key: Hello and Value: Ciao
Now you can use this under your Views folder in Index.cshtml file:
Example: <text>#Localizer["Hello"]</text>
Now you can create a form in your website, whereever you'd like to create, lets assume you want to create it on your navbar
Go to your navbar.cshtml (In my example, this can be any different page in your site)
And add following top of your .cshtml
#{
var requestCulture = Context.Features.Get<IRequestCultureFeature>();
var cultureItems = LocOptions.Value.SupportedUICultures
.Select(c => new SelectListItem { Value = c.Name, Text = c.DisplayName })
.ToList();
}
also create a form for post data to LanguageController.cs -> SetLanguage(): (Below form's design could be the worse... sorry)
<form asp-controller="Language" asp-action="SetLanguage" id="selectLanguage"
asp-route-returnURL="#Context.Request.Path" method="post" class="form-horizontal" role="form">
<ul class="navbar mt-auto">
<li class="nav-item dropdown">
<button name="culture" value="#cultureItems[1].Value" type="submit">
<span class="flag-icon flag-icon-us mr-1"> </span>
#cultureItems[0].Value
</button>
<br />
<button name="culture" value="#cultureItems[2].Value" type="submit">
<span class="flag-icon flag-icon-it mr-1"> </span>
#cultureItems[1].Value</button>
</li>
</ul>
</form>
Note: Because of .resx files are in XML format you can use them everywhere and its so light to use.
Related
With Blazor I've made a Blazor Server App, where there is a page that can upload txt files.
The files are then stored in the \wwwroot directory.
Now i'm trying to find out how to get some edit function on the page,
so the strings can be edited from there.
Here is some code that shows I want every 3 in my txt file replaced.
#code
{
public void editFile()
{
File.WriteAllText(#"\wwwroot\DEJLIGER.txt", Regex.Replace(File.ReadAllText(#"\wwwroot\DEJLIGER.txt"), "3", "replacementString"));
}
}
I'm just not sure how I can get the an edit function on the page that can replace the strings in a file.
I hope you are working on a hobby project, otherwise this is not a very good approach to store user data.
Under that assumption, this example probably helps you out:
<textarea #bind="_contents"></textarea>
<button #onclick="OpenFile">Open file</button>
<button #onclick="SaveFile">Save file</button>
#code
{
string _contents { get; set; } = string.Empty;
void OpenFile()
{
_contents = File.ReadAllText(#"\wwwroot\DEJLIGER.txt");
}
void SaveFile()
{
File.WriteAllText(#"\wwwroot\DEJLIGER.txt", _contents);
}
}
I'm struggling with localization in my new .NET Core project.
I have 2 projects:
DataAccess project with Models and DataAnnotations (e.g. RequiredAttribute)
Web project with MVC views etc.
My wish is to localize all validation attributes globally in one single place to have the similar behavior like MVC 5. Is this possible?
I do not want to have separate language files for Models/Views etc.
Microsofts documentation is not very clear on using SharedResources.resx file with localized DataAnnotation messages.
In MVC 5 I didn't take care of it. I only needed to set the locale to my language and everything was fine.
I tried setting the ErrorMessageResourceName and ErrorMessageResourceType to my shared resource file name "Strings.resx" and "Strings.de.resx" in the DataAccess project:
[Required(ErrorMessageResourceName = "RequiredAttribute_ValidationError", ErrorMessageResourceType = typeof(Strings))]
I also tried the setting name to be RequiredAttribute_ValidationError - but it's not working.
I already added .AddDataAnnotationsLocalization() in Startup.cs - but it seems to do nothing.
I've read several articles but I couldn't find the cause why it's not working.
EDIT:
What I have so far:
1.) LocService class
public class LocService
{
private readonly IStringLocalizer _localizer;
public LocService(IStringLocalizerFactory factory)
{
_localizer = factory.Create(typeof(Strings));
}
public LocalizedString GetLocalizedHtmlString(string key)
{
return _localizer[key];
}
}
2.) Added Folder "Resources" with Strings.cs (empty class with dummy constructor)
3.) Added Strings.de-DE.resx file with one item "RequiredAttribute_ValidationError"
4.) Modified my Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<MessageService>();
services.AddDbContext<DataContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddSingleton<LocService>();
services.AddLocalization(options => options.ResourcesPath = "Resources");
services.AddMvc()
.AddJsonOptions(options => options.SerializerSettings.ContractResolver = new DefaultContractResolver())
.AddDataAnnotationsLocalization(
options =>
{
options.DataAnnotationLocalizerProvider = (type, factory) => factory.Create(typeof(Strings));
});
services.Configure<RequestLocalizationOptions>(
opts =>
{
var supportedCultures = new List<CultureInfo>
{
new CultureInfo("de-DE"),
};
opts.DefaultRequestCulture = new RequestCulture("de-DE");
// Formatting numbers, dates, etc.
opts.SupportedCultures = supportedCultures;
// UI strings that we have localized.
opts.SupportedUICultures = supportedCultures;
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseBrowserLink();
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
var locOptions = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();
app.UseRequestLocalization(locOptions.Value);
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
I've followed the instructions here but it doesn't work:
https://damienbod.com/2017/11/01/shared-localization-in-asp-net-core-mvc/
Please keep in mind that my Models are kept in a separate project.
As #Sven points out in his comment to Tseng's answer it still requires that you specify an explicit ErrorMessage, which gets quite tedious.
The problem arises from the logic ValidationAttributeAdapter<TAttribute>.GetErrorMessage() uses to decide whether to use the provided IStringLocalizer or not.
I use the following solution to get around that issue:
Create a custom IValidationAttributeAdapterProvider implementation that uses the default ValidationAttributeAdapterProvider like this:
public class LocalizedValidationAttributeAdapterProvider : IValidationAttributeAdapterProvider
{
private readonly ValidationAttributeAdapterProvider _originalProvider = new ValidationAttributeAdapterProvider();
public IAttributeAdapter GetAttributeAdapter(ValidationAttribute attribute, IStringLocalizer stringLocalizer)
{
attribute.ErrorMessage = attribute.GetType().Name.Replace("Attribute", string.Empty);
if (attribute is DataTypeAttribute dataTypeAttribute)
attribute.ErrorMessage += "_" + dataTypeAttribute.DataType;
return _originalProvider.GetAttributeAdapter(attribute, stringLocalizer);
}
}
Register the adapter in Startup.ConfigureServices() Before calling AddMvc():
services.AddSingleton<Microsoft.AspNetCore.Mvc.DataAnnotations.IValidationAttributeAdapterProvider, LocalizedValidationAttributeAdapterProvider>();
I prefer to use "stricter" resource names based on the actual attributes, so the code above will look for resource names like "Required" and "DataType_Password", but this can of course be customized in many ways.
If you prefer resources names based on the default messages of the Attributes you could instead write something like:
attribute.ErrorMessage = attribute.FormatErrorMessage("{0}");
I tried setting the ErrorMessageResourceName and ErrorMessageResourceType to my shared resource file name "Strings.resx" and "Strings.de.resx" in the DataAccess project:
[Required(ErrorMessageResourceName = "RequiredAttribute_ValidationError", ErrorMessageResourceType = typeof(Strings))]
I also tried the setting name to be RequiredAttribute_ValidationError - but it's not working.
You were on the right track, but you don't necessarily need to set ErrorMessageResourceName / ErrorMessageResourceType properties.
Was we can see in the source code of ValidationAttributeAdapter<TAttribute>, the conditions to use the _stringLocalizer verison is when ErrorMessage is not null and ErrorMessageResourceName/ErrorMessageResourceType are null.
In other words, when you don't set any properties or only ErrorMessage. So a plain [Required] should just work (see source where is passed to the base classes constructor).
Now, when we look at the DataAnnotations resource file we see that the name is set to "RequiredAttribute_ValidationError" and the value to "The {0} field is required." which is the default English translation.
Now if you use "RequiredAttribute_ValidationError" with the German translation in your "Strings.de-DE.resx" (or just Strings.resx as fallback), it should work with the corrected namespace from the comments.
So using the above configuration and the strings from the GitHub repository you should be able to make the localization work without extra attributes.
It turned out that ValidationAttributeAdapterProvider approach doesn't work as it is meant to be used for "client side validation attributes" only (which doesn't make much sense to me because the attributes are specified on the server model).
But I found a solution that works to override all attributes with custom messages. It also is able to inject field name translations without spitting [Display] all over the place. It's convention-over-configuration in action.
Also, as a bonus, this solution overrides default model binding error texts that are used even before validation takes place. One caveat - if you receive JSON data, then Json.Net errors will be merged into ModelState errors and default binding errors won't be used. I haven't yet figured out how to prevent this from happening.
So, here are three classes you will need:
public class LocalizableValidationMetadataProvider : IValidationMetadataProvider
{
private IStringLocalizer _stringLocalizer;
private Type _injectableType;
public LocalizableValidationMetadataProvider(IStringLocalizer stringLocalizer, Type injectableType)
{
_stringLocalizer = stringLocalizer;
_injectableType = injectableType;
}
public void CreateValidationMetadata(ValidationMetadataProviderContext context)
{
// ignore non-properties and types that do not match some model base type
if (context.Key.ContainerType == null ||
!_injectableType.IsAssignableFrom(context.Key.ContainerType))
return;
// In the code below I assume that expected use of ErrorMessage will be:
// 1 - not set when it is ok to fill with the default translation from the resource file
// 2 - set to a specific key in the resources file to override my defaults
// 3 - never set to a final text value
var propertyName = context.Key.Name;
var modelName = context.Key.ContainerType.Name;
// sanity check
if (string.IsNullOrEmpty(propertyName) || string.IsNullOrEmpty(modelName))
return;
foreach (var attribute in context.ValidationMetadata.ValidatorMetadata)
{
var tAttr = attribute as ValidationAttribute;
if (tAttr != null)
{
// at first, assume the text to be generic error
var errorName = tAttr.GetType().Name;
var fallbackName = errorName + "_ValidationError";
// Will look for generic widely known resource keys like
// MaxLengthAttribute_ValidationError
// RangeAttribute_ValidationError
// EmailAddressAttribute_ValidationError
// RequiredAttribute_ValidationError
// etc.
// Treat errormessage as resource name, if it's set,
// otherwise assume default.
var name = tAttr.ErrorMessage ?? fallbackName;
// At first, attempt to retrieve model specific text
var localized = _stringLocalizer[name];
// Some attributes come with texts already preset (breaking the rule 3),
// even if we didn't do that explicitly on the attribute.
// For example [EmailAddress] has entire message already filled in by MVC.
// Therefore we first check if we could find the value by the given key;
// if not, then fall back to default name.
// Final attempt - default name from property alone
if (localized.ResourceNotFound) // missing key or prefilled text
localized = _stringLocalizer[fallbackName];
// If not found yet, then give up, leave initially determined name as it is
var text = localized.ResourceNotFound ? name : localized;
tAttr.ErrorMessage = text;
}
}
}
}
public class LocalizableInjectingDisplayNameProvider : IDisplayMetadataProvider
{
private IStringLocalizer _stringLocalizer;
private Type _injectableType;
public LocalizableInjectingDisplayNameProvider(IStringLocalizer stringLocalizer, Type injectableType)
{
_stringLocalizer = stringLocalizer;
_injectableType = injectableType;
}
public void CreateDisplayMetadata(DisplayMetadataProviderContext context)
{
// ignore non-properties and types that do not match some model base type
if (context.Key.ContainerType == null ||
!_injectableType.IsAssignableFrom(context.Key.ContainerType))
return;
// In the code below I assume that expected use of field name will be:
// 1 - [Display] or Name not set when it is ok to fill with the default translation from the resource file
// 2 - [Display(Name = x)]set to a specific key in the resources file to override my defaults
var propertyName = context.Key.Name;
var modelName = context.Key.ContainerType.Name;
// sanity check
if (string.IsNullOrEmpty(propertyName) || string.IsNullOrEmpty(modelName))
return;
var fallbackName = propertyName + "_FieldName";
// If explicit name is missing, will try to fall back to generic widely known field name,
// which should exist in resources (such as "Name_FieldName", "Id_FieldName", "Version_FieldName", "DateCreated_FieldName" ...)
var name = fallbackName;
// If Display attribute was given, use the last of it
// to extract the name to use as resource key
foreach (var attribute in context.PropertyAttributes)
{
var tAttr = attribute as DisplayAttribute;
if (tAttr != null)
{
// Treat Display.Name as resource name, if it's set,
// otherwise assume default.
name = tAttr.Name ?? fallbackName;
}
}
// At first, attempt to retrieve model specific text
var localized = _stringLocalizer[name];
// Final attempt - default name from property alone
if (localized.ResourceNotFound)
localized = _stringLocalizer[fallbackName];
// If not found yet, then give up, leave initially determined name as it is
var text = localized.ResourceNotFound ? name : localized;
context.DisplayMetadata.DisplayName = () => text;
}
}
public static class LocalizedModelBindingMessageExtensions
{
public static IMvcBuilder AddModelBindingMessagesLocalizer(this IMvcBuilder mvc,
IServiceCollection services, Type modelBaseType)
{
var factory = services.BuildServiceProvider().GetService<IStringLocalizerFactory>();
var VL = factory.Create(typeof(ValidationMessagesResource));
var DL = factory.Create(typeof(FieldNamesResource));
return mvc.AddMvcOptions(o =>
{
// for validation error messages
o.ModelMetadataDetailsProviders.Add(new LocalizableValidationMetadataProvider(VL, modelBaseType));
// for field names
o.ModelMetadataDetailsProviders.Add(new LocalizableInjectingDisplayNameProvider(DL, modelBaseType));
// does not work for JSON models - Json.Net throws its own error messages into ModelState :(
// ModelBindingMessageProvider is only for FromForm
// Json works for FromBody and needs a separate format interceptor
DefaultModelBindingMessageProvider provider = o.ModelBindingMessageProvider;
provider.SetValueIsInvalidAccessor((v) => VL["FormatHtmlGeneration_ValueIsInvalid", v]);
provider.SetAttemptedValueIsInvalidAccessor((v, x) => VL["FormatModelState_AttemptedValueIsInvalid", v, x]);
provider.SetMissingBindRequiredValueAccessor((v) => VL["FormatModelBinding_MissingBindRequiredMember", v]);
provider.SetMissingKeyOrValueAccessor(() => VL["FormatKeyValuePair_BothKeyAndValueMustBePresent" ]);
provider.SetMissingRequestBodyRequiredValueAccessor(() => VL["FormatModelBinding_MissingRequestBodyRequiredMember"]);
provider.SetNonPropertyAttemptedValueIsInvalidAccessor((v) => VL["FormatModelState_NonPropertyAttemptedValueIsInvalid", v]);
provider.SetNonPropertyUnknownValueIsInvalidAccessor(() => VL["FormatModelState_UnknownValueIsInvalid"]);
provider.SetUnknownValueIsInvalidAccessor((v) => VL["FormatModelState_NonPropertyUnknownValueIsInvalid", v]);
provider.SetValueMustNotBeNullAccessor((v) => VL["FormatModelBinding_NullValueNotValid", v]);
provider.SetValueMustBeANumberAccessor((v) => VL["FormatHtmlGeneration_ValueMustBeNumber", v]);
provider.SetNonPropertyValueMustBeANumberAccessor(() => VL["FormatHtmlGeneration_NonPropertyValueMustBeNumber"]);
});
}
}
In ConfigureServices in your Startup.cs file:
services.AddMvc( ... )
.AddModelBindingMessagesLocalizer(services, typeof(IDtoModel));
I have used my custom empty IDtoModel interface here and applied it to all my API models that will need the automatic localization for errors and field names.
Create a folder Resources and put empty classes ValidationMessagesResource and FieldNamesResource inside it.
Create ValidationMessagesResource.ab-CD.resx and FieldNamesResource .ab-CD.resx files (replace ab-CD with your desired culture).
Fill in the values for the keys you need, e.g. FormatModelBinding_MissingBindRequiredMember, MaxLengthAttribute_ValidationError ...
When launching the API from a browser, make sure to modify accept-languages header to be your culture name, otherwise Core will use it instead of defaults. For API that needs single language only, I prefer to disable culture providers altogether using the following code:
private readonly CultureInfo[] _supportedCultures = new[] {
new CultureInfo("ab-CD")
};
...
var ci = new CultureInfo("ab-CD");
// can customize decimal separator to match your needs - some customers require to go against culture defaults and, for example, use . instead of , as decimal separator or use different date format
/*
ci.NumberFormat.NumberDecimalSeparator = ".";
ci.NumberFormat.CurrencyDecimalSeparator = ".";
*/
_defaultRequestCulture = new RequestCulture(ci, ci);
...
services.Configure<RequestLocalizationOptions>(options =>
{
options.DefaultRequestCulture = _defaultRequestCulture;
options.SupportedCultures = _supportedCultures;
options.SupportedUICultures = _supportedCultures;
options.RequestCultureProviders = new List<IRequestCultureProvider>(); // empty list - use default value always
});
unfortunately, it is not that simple to localize all error messages for data attributes in one single place! because there are different types of error messages,
Error messages for standard data attributes:
[Required]
[Range]
[StringLength]
[Compare]
...etc.
Error messages for ModelBinding:
ValueIsInvalid
ValueMustNotBeNull
PropertyValueMustBeANumber
...etc.
and Identity error messages:
DuplicateEmail
DuplicateRoleName
InvalidUserName
PasswordRequiresLower
PasswordRequiresUpper
...etc
each must be configured in the startup file. Additionaly client side validation must be considered as well.
you may check these articles for more details, it contains live demo and sample project on GitHub:
Developing multicultural web application:
http://www.ziyad.info/en/articles/10-Developing_Multicultural_Web_Application
Localizing data annotations:
http://www.ziyad.info/en/articles/16-Localizing_DataAnnotations
Localizing ModelBinding error messages:
http://www.ziyad.info/en/articles/18-Localizing_ModelBinding_Error_Messages
Localizing identity error messages:
http://www.ziyad.info/en/articles/20-Localizing_Identity_Error_Messages
and client side validation:
http://ziyad.info/en/articles/19-Configuring_Client_Side_Validation
hope it helps :)
public class RequiredExAttribute : RequiredAttribute
{
public override string FormatErrorMessage(string name)
{
string Format = GetAFormatStringFromSomewhereAccordingToCurrentCulture();
return string.Format(Format, name);
}
}
...
public class MyModel
{
[RequiredEx]
public string Name { get; set; }
}
I have created an web application of Tenders with a DropDownList of companies' name. These names I take from a specific directory, put into a array and copy to a List and I added the string "-Add new-" in order to create an action when this option is chosen by the user.
First question: How can I create a action that opens a little window with a text box when the user chooses "-Add new-", and after writing the name and clicking in "Add", it creates a new folder in my "\My network directory\"?
Second question: As I said before, the DropDownList of companies' name is from "\My network directory\", how can I pass the value chosen by the user (the company name) and shows in another DropDownList the sub-directories of the folder (company) chosen?
//The controller
public class TenderController : Controller
{
//
// GET: /Tender/
public ActionResult AddNewTender()
{
//Get companies directory and put it into a string array
string[] compNameArray = Directory.GetDirectories(#"//My network directory\");
int i = 0;
foreach (string txtName in compNameArray)
{
//Copy to another string every directory name only with the las name file (the company name)
string txtDirName = txtName.Substring(txtName.LastIndexOf(#"\") + 1);
//Update the companies name array with the companies name only
compNameArray[i] = txtDirName;
i++;
}
//Copy the companies name array to a list
List<string> compList = new List<string>(compNameArray);
//Remove from the list the names above
compList.Remove("New folder");
//Add the "add new" option to the list
compList.Add("-Add new-");
ViewBag.ListOfCompanies = compList;
return View();
}
The view:
<td dir="rtl">
#Html.DropDownList("companyName", new SelectList(ViewBag.ListOfCompanies, Model))
</td>
The page:
It looks like this
First question:
Run javascript after dropdownlist change.
In the Javascript you should check the dropdown value.
Then you can create a button from the Javascript.
After clicking "add", create an action in the controller to create the new folder.
Second question:
The same trick as the first question, use onchange event to run Javascript.
In the Javascript you can add values to dropdownlists.
In my mvc solution I was originally using a viewModel to hold an IEnumerable of SelectListItems. These would be used to populate a dropdownfor element like below
#Html.DropDownListFor(model => model.Type, Model.PrimaryTypeList, new { data_acc_type = "account", data_old = Model.Type, #class = "js-primary-account-type" })
the problem being that whenever I had to return this view, the list would need re-populating with something pretty heavy like the following:
if(!ModelState.IsValid){
using (var typeRepo = new AccountTypeRepository())
{
var primTypes = typeRepo.GetAccountTypes();
var primtype = primTypes.SingleOrDefault(type => type.Text == model.Type);
model.PrimaryTypeList =
primTypes
.Select(type => new SelectListItem()
{
Value = type.Text,
Text = type.Text
}).ToList();
}
return View(model);
}
It seemed silly to me to have to rewrite - or even re-call (if put into a method) the same code every postback. - the same applies for the ViewBag as i have about 6 controllers that call this same view due to inheritance and the layout of my page.
At the moment i'm opting to put the call actually in my razor. but this feels wrong and more like old-school asp. like below
#{
ViewBag.Title = "Edit Account " + Model.Name;
List<SelectListItem> primaryTypes = null;
using (var typeRepo = new AccountTypeRepository())
{
primaryTypes =
typeRepo.GetAccountTypes()
.Select(t => new SelectListItem()
{
Value = t.Text,
Text = t.Text
}).ToList();
}
#Html.DropDownListFor(model => model.Type, primaryTypes, new { data_acc_type = "account", data_old = Model.Type, #class = "js-primary-account-type" })
Without using something completely bizarre. would there be a better way to go about this situation?
UPDATE: While semi-taking onboard the answer from #Dawood Awan below. my code is somewhat better, still in the view though and i'm 100% still open to other peoples ideas or answers.
Current code (Razor and Controller)
public static List<SelectListItem> GetPrimaryListItems(List<AccountType> types)
{
return types.Select(t => new SelectListItem() { Text = t.Text, Value = t.Text }).ToList();
}
public static List<SelectListItem> GetSecondaryListItems(AccountType type)
{
return type == null?new List<SelectListItem>(): type.AccountSubTypes.Select(t => new SelectListItem() { Text = t.Text, Value = t.Text }).ToList();
}
#{
ViewBag.Title = "Add New Account";
List<SelectListItem> secondaryTypes = null;
List<SelectListItem> primaryTypes = null;
using (var typeRepo = new AccountTypeRepository())
{
var primTypes = typeRepo.GetAccountTypes();
primaryTypes = AccountController.GetPrimaryListItems(primTypes);
secondaryTypes = AccountController.GetSecondaryListItems(primTypes.SingleOrDefault(t => t.Text == Model.Type));
}
}
In practice, you need to analyse where you app is running slow and speed up those parts first.
For starters, take any code like that out of the view and put it back in the controller. The overhead of using a ViewModel is negligible (speed-wise). Better to have all decision/data-fetching code in the controller and not pollute the view (Views should only know how to render a particular "shape" of data, not where it comes from).
Your "Something pretty heavy" comment is pretty arbitary. If that query was, for instance, running across the 1Gb connections on an Azure hosted website, you would not notice or care that much. Database caching would kick in too to give it a boost.
Having said that, this really is just a caching issue and deciding where to cache it. If the data is common to all users, a static property (e.g. in the controller, or stored globally) will provide fast in-memory reuse of that static list.
If the data changes frequently, you will need to provide for refreshing that in-memory cache.
If you used IOC/injection you can specific a single static instance shared across all requests.
Don't use per-session data to store static information. That will slow down the system and run you out of memory with loads of users (i.e. it will not scale well).
If the DropDown Values don't change it is better to save in Session[""], then you can access in you View, controller etc.
Create a class in a Helpers Folder:
public class CommonDropDown
{
public string key = "DropDown";
public List<SelectListItem> myDropDownItems
{
get { return HttpContext.Current.Session[key] == null ? GetDropDown() : (List<SelectListItem>)HttpContext.Current.Session[key]; }
set { HttpContext.Current.Session[key] = value; }
}
public List<SelectListItem> GetDropDown()
{
// Implement Dropdown Logic here
// And set like this:
this.myDropDownItems = DropdownValues;
}
}
Create a Partial View in Shared Folder ("_dropDown.cshtml"):
With something like this:
#{
// Add Reference to this Folder
var items = Helpers.CommonDropDown.myDropDownItems;
}
#Html.DropDownList("ITems", items, "Select")
And then at the top of each page:
#Html.Partial("_dropDown.cshtml")
I have a controller, which creates breadcrumbs as follows:
Software > Windows 7 > Outlook 2007
The code to create this is:
ViewBag.breadcrumbs = string.Join(" > ", cbh.Select(i => i.Title));
Is there a straightforward way of making the breadcrumbs hyperlinks, which would point to (i.ParentID) ie:
Software -> forum/index/12
Windows 7 -> forum/index/19
Outlook 2007 -> forum/index/23
Or should I just loop through cbh and manually build <a href=...> strings, and pass those to the view?
Thank you,
Mark
Your best bet is to put the required items into the model then loop through them.
Try something like this:
Model
public class Model
{
public struct BreadCrumb
{
public string Title;
public string Url;
}
public List<BreadCrumb> Breadcrumbs { get; set; }
}
View
#{ int index = 0; }
#foreach(var crumb in this.Model.Breadcrumbs)
{
#(crumb.Title)
if(index < this.Model.Breadcrumbs.Count - 1)
{
<span>></span>
}
index++;
}
Yes, you should build your breadcrumb links in the view. If it helps, you can create a BreadCrumbModel class (if you don't already have one).
ViewBag.breadcrumbs = cbh.Select(i => new BreadCrumbModel()
{
Id = i.Id,
Title = i.Title
});
#{
var printSeparator = false;
}
#foreach(BreadCrumbModel bc in ViewBag.breadcrumbs)
{
#if(printSeparator)
{
<span class="breadcrumb-separator"> > </span>
}
<span class="breadcrumb">
#Html.ActionLink(bc.Title, "index", "forum", new { id = bc.Id });
</span>
#{
printSeparator = true;
}
}
If you want to have breadcrumbs between different controllers and actions (not just forum / index), then add those as properties of your BreadCrumbModel.