I have a line chart with many lines inside, the legend is displayed below the chart area. The problem is that the legend takes a lot of space vertically, and this space is consumed from the fix total height of the chart. In extreme cases, there is no space left for the chart itself. Is there an option to have a fix size for the chart area, and add the legend's height in addition to this?
Thanks in advance!
You can try to mess around with these values. Here's an example:
<kendo-chart-legend
[labels]="{font:'8pt sans-serif'}"
[position]="'start"
[orientation]="'vertical'">
</kendo-chart-legend>
Full example with Input Params.
HTML:
<kendo-chart (render)="onRender($event)">
<kendo-chart-legend
[labels]="{font:'12pt sans-serif'}"
[position]="legendPosition"
[orientation]="legendOrientation">
</kendo-chart-legend>
<kendo-chart-title
[text]="titleDescription"
font="12pt sans-serif"
[align]="titleAlign">
</kendo-chart-title>
<kendo-chart-tooltip [shared]="sharedTooltip" [format]="formatTooltip"></kendo-chart-tooltip>
<kendo-chart-category-axis>
<kendo-chart-category-axis-item
name="categoryAxis"
[labels]="{font:'10pt sans-serif'}"
[visible]="labelStep !== 0">
<kendo-chart-category-axis-item-labels [step]="labelStep" [content]="labelContentString" [rotation]="labelRotation"></kendo-chart-category-axis-item-labels>
</kendo-chart-category-axis-item>
</kendo-chart-category-axis>
<kendo-chart-value-axis>
<kendo-chart-value-axis-item
name="valueAxis"
[plotBands]="plotBands"
[labels]="{font:'12pt sans-serif'}">
<kendo-chart-value-axis-item-labels [content]="labelContentString"></kendo-chart-value-axis-item-labels>
</kendo-chart-value-axis-item>
</kendo-chart-value-axis>
<kendo-chart-series>
<ng-container *ngIf="groupByField">
<kendo-chart-series-item *ngFor="let item of series"
[type]="type"
[data]="item.items"
[name]="item.value"
[markers]="{visible: useMarkers}"
field="value"
categoryField="label">
</kendo-chart-series-item>
</ng-container>
<ng-container *ngIf="!groupByField">
<kendo-chart-series-item
[type]="type"
[data]="series"
[markers]="{visible: useMarkers}"
field="value"
categoryField="label">
</kendo-chart-series-item>
</ng-container>
</kendo-chart-series>
</kendo-chart>
Component:
#Input() public data: PentahoResponse;
#Input() public type: string;
#Input() public groupByField: string;
#Input() public labelField: string;
#Input() public valueField: string;
#Input() public excludeValues: Array<{ field: string, value: string }> = [];
#Input() public titleDescription: string = ""; // text (disabled if left empty)
#Input() public titleAlign: string = "start"; // start middle end
#Input() public legendPosition: string; // top bottom left right
#Input() public legendOrientation: string; // horizontal vertical
#Input() public formatTooltip: string = "{0}"; // {0} <-- value
#Input() public sharedTooltip: boolean = false; // false true
#Input() public useMarkers: boolean = true; // false true
#Input() public preferredLabelAmount: number = 15;
#Input() public labelRotation: number;
#Input() public lines: Array<{ value: number, color: string, opacity?: number }>;
private series: any[] = [];
private labelStep: number = 1;
constructor(
#Inject(AppService) private readonly appService: AppService
) { }
public onRender(args) {
if (this.lines && this.type === "line") {
const chart = args.sender.instance; // Remove ".instance" when upgraded
// get the axes
const valueAxis = chart.findAxisByName("valueAxis");
const categoryAxis = chart.findAxisByName("categoryAxis");
// get the coordinates of the entire category axis range
const range = categoryAxis.range();
const categorySlot = categoryAxis.slot(range.min, range.max);
const group = new Group();
this.lines.forEach((plotLine) => {
// get the coordinates of the value at which the plot band will be rendered
const valueSlot = valueAxis.slot(plotLine.value);
// draw the plot band based on the found coordinates
const line = new Path({
stroke: {
color: plotLine.color,
width: 3,
opacity: plotLine.opacity !== undefined ? plotLine.opacity : 1
}
}).moveTo(valueSlot.origin)
.lineTo(categorySlot.topRight().x, valueSlot.origin.y);
group.append(line);
});
// Draw on the Chart surface to overlay the series
chart.surface.draw(group);
}
}
private getData() {
const metadata = this.data.metadata;
let data = this.data.resultset;
this.excludeValues.forEach((exclude) => {
const fieldIndex: number = _.findIndex(metadata, (x) => x.colName === exclude.field);
if (fieldIndex !== -1) {
data = _.filter(data, (x) => x[fieldIndex] !== exclude.value);
}
});
const labelIndex = _.findIndex(metadata, (x) => x.colName === this.labelField);
const valueIndex = _.findIndex(metadata, (x) => x.colName === this.valueField);
const groupByFieldIndex = _.findIndex(metadata, (x) => x.colName === this.groupByField);
data.forEach((row) => {
this.series.push({
groupBy: groupByFieldIndex !== -1 ? row[groupByFieldIndex] : row[labelIndex],
value: row[valueIndex],
label: row[labelIndex],
groupByKey: groupByFieldIndex !== -1 ? row[groupByFieldIndex] : row[labelIndex],
valueKey: row[valueIndex],
labelKey: row[labelIndex]
});
});
this.series = this.groupByField !== undefined ? groupBy(this.series, [{ field: "groupBy" }]) : this.series;
}
private labelContentString = (e: any) => {
if (e.value instanceof Date) {
return moment(e.value).format("L LT");
} else if (e.value instanceof String) {
return this.appService.translate(e.value);
}
return e.value;
}
public ngOnInit() {
this.getData();
if (this.preferredLabelAmount > 0) {
let maxTotalItems = 0;
if (this.groupByField !== undefined) {
this.series.forEach((serie: GroupResult) => {
if (maxTotalItems < serie.items.length) {
maxTotalItems = serie.items.length;
}
});
} else {
maxTotalItems = this.series.length;
}
this.labelStep = Math.round(maxTotalItems / this.preferredLabelAmount);
} else if (this.preferredLabelAmount === 0) {
this.labelStep = 0;
}
}
https://www.telerik.com/kendo-angular-ui/components/charts/api/Legend/
https://www.telerik.com/kendo-angular-ui/components/charts/api/LegendLabels/
https://www.telerik.com/kendo-angular-ui/components/charts/api/LegendItem/
https://www.telerik.com/kendo-angular-ui/components/charts/api/LegendMarkers/
Full API here.
For a similar situation I just set the visible false for the legend of the chart in question and used a new chart widget to just display a legend.
legend: {
visible: false
}
For just displaying a legend
$("#legendChart").kendoChart({
legend: {
position: "top",
labels: {
padding: {
left: paddingLeftLegendLabel
}
}
},
series: [
{ name: LowName },
{ name: MediumName },
{ name: HighName },
],
seriesColors: ['#DCD267', '#DC8C67', '#DC6967'],
chartArea: {
height: 35
},
axisDefaults: {
visible: false,
majorGridLines: {
visible: false
}
},
});
Related
I have a search for a word option above the accordion, so users can search for a specific word in the accordion and those accordions that has the words matched are expanded and the searched word is highlighted. For now the search and highlight the string part is working, but the accordions doesn't expand per the indexes selected. Only the first accordion opens by default for all search result.enter image description here
Can you help with what i'm missing in the below code?
terms.ts
import { Component, OnInit } from '#angular/core';
import { CommonService } from '../service/common.service';
import { GlossaryOfTerms } from '../model/GlossaryOfTerms.model';
#Component({
selector: 'app-terms',
templateUrl: './terms.component.html',
styleUrls: ['./terms.component.scss']
})
export class GlossaryOfTermsComponent implements OnInit {
search: string = "";
gTerms: GlossaryOfTerms[] = [];
step = 0;
constructor(private commonService: CommonService) {}
ngOnInit(): void {
this.getTerms();
}
public OnSearched(searchTerm: string) {
this.search = searchTerm;
this.findSearchedValue();
}
getTerms(): void {
this.commonService.getGlossaryOfTerms().subscribe((terms) => {
this.gTerms = terms;
console.log("terms", this.gTerms );
});
}
findSearchedValue(): void {
this.gTerms?.forEach((terms, index) => {
var decription = terms.description;
var name = terms.name;
if(this.search != '' || undefined) {
if ((decription?.includes(this.search)) || (name?.includes( this.search) )) {
console.log("openindex", index);
this.step = index;
} else {
console.log("closeindex", index);
this.step = index;
}
}
});
}
}
<app-search #searchedValue (searched)="OnSearched($event)"></app-search>
<mat-accordion multi="true">
<mat-expansion-panel #accordianContainer *ngFor="let term of gTerms; let i = index" [expanded]="step === i">
<mat-expansion-panel-header>
<mat-panel-title>
<p [innerHTML]="term.name | highlightSearch: search"></p>
</mat-panel-title>
</mat-expansion-panel-header>
<div class="content">
<div class="content-text">
<p [innerHTML]="term.description | highlightSearch: search"></p>
</div>
</div>
</mat-expansion-panel>
</mat-accordion>
I found the solution to my question, as someone can find it helpful.
Open the select
findSearchedValue(): void {
this.step = 0;
var searchTerm = this.search.toLowerCase();
this.gTerms?.forEach((terms, index) => {
if (this.search != '' || undefined) {
if ((terms.description?.toLowerCase().includes(searchTerm)) || (terms.name?.toLowerCase().includes(searchTerm))) {
this.step = ++this.step;
this.openSelected(index, this.step);
}
}
});
}
openSelected(indexed: number, step: number): boolean {
return this.step > indexed ? true : false;
}
*ngFor="let term of gTerms; let i=index" [expanded]="openSelected(i, step)"
I have attached screen short I want to implement this stepped progress bar in Xamarin.iOS.
Please help any source code regarding to this process in Xamarin.iOS.Thanks
You could create a custom step progress bar.
public class StepProgressBarControl : StackLayout
{
Button _lastStepSelected;
public static readonly BindableProperty StepsProperty =BindableProperty.Create(nameof(Steps), typeof(int), typeof(StepProgressBarControl), 0);
public static readonly BindableProperty StepSelectedProperty =BindableProperty.Create(nameof(StepSelected), typeof(int), typeof(StepProgressBarControl), 0, defaultBindingMode: BindingMode.TwoWay);
public static readonly BindableProperty StepColorProperty = BindableProperty.Create(nameof(StepColor), typeof(Xamarin.Forms.Color), typeof(StepProgressBarControl), Color.Black, defaultBindingMode: BindingMode.TwoWay);
public Color StepColor
{
get { return (Color)GetValue(StepColorProperty); }
set { SetValue(StepColorProperty, value); }
}
public int Steps
{
get { return (int)GetValue(StepsProperty); }
set { SetValue(StepsProperty, value); }
}
public int StepSelected
{
get { return (int)GetValue(StepSelectedProperty); }
set { SetValue(StepSelectedProperty, value); }
}
public StepProgressBarControl()
{
Orientation = StackOrientation.Horizontal;
HorizontalOptions = LayoutOptions.FillAndExpand;
Padding = new Thickness(10, 0);
Spacing = 0;
AddStyles();
}
protected override void OnPropertyChanged(string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == StepsProperty.PropertyName)
{
for (int i = 0; i < Steps; i++)
{
var button = new Button()
{
Text = $"{i + 1}", ClassId= $"{i + 1}",
Style = Resources["unSelectedStyle"] as Style
};
button.Clicked += Handle_Clicked;
this.Children.Add(button);
if (i < Steps - 1)
{
var separatorLine = new BoxView()
{
BackgroundColor = Color.Silver,
HeightRequest = 1,
WidthRequest=5,
VerticalOptions = LayoutOptions.Center,
HorizontalOptions = LayoutOptions.FillAndExpand
};
this.Children.Add(separatorLine);
}
}
}else if(propertyName == StepSelectedProperty.PropertyName){
var children= this.Children.First(p => (!string.IsNullOrEmpty(p.ClassId) && Convert.ToInt32(p.ClassId) == StepSelected));
if(children != null) SelectElement(children as Button);
}else if(propertyName == StepColorProperty.PropertyName){
AddStyles();
}
}
void Handle_Clicked(object sender, System.EventArgs e)
{
SelectElement(sender as Button);
}
void SelectElement(Button elementSelected){
if (_lastStepSelected != null) _lastStepSelected.Style = Resources["unSelectedStyle"] as Style;
elementSelected.Style = Resources["selectedStyle"] as Style;
StepSelected = Convert.ToInt32(elementSelected.Text);
_lastStepSelected = elementSelected;
}
void AddStyles(){
var unselectedStyle = new Style(typeof(Button))
{
Setters = {
new Setter { Property = BackgroundColorProperty, Value = Color.Transparent },
new Setter { Property = Button.BorderColorProperty, Value = StepColor },
new Setter { Property = Button.TextColorProperty, Value = StepColor },
new Setter { Property = Button.BorderWidthProperty, Value = 0.5 },
new Setter { Property = Button.BorderRadiusProperty, Value = 20 },
new Setter { Property = HeightRequestProperty, Value = 40 },
new Setter { Property = WidthRequestProperty, Value = 40 }
}
};
var selectedStyle = new Style(typeof(Button))
{
Setters = {
new Setter { Property = BackgroundColorProperty, Value = StepColor },
new Setter { Property = Button.TextColorProperty, Value = Color.White },
new Setter { Property = Button.BorderColorProperty, Value = StepColor },
new Setter { Property = Button.BorderWidthProperty, Value = 0.5 },
new Setter { Property = Button.BorderRadiusProperty, Value = 20 },
new Setter { Property = HeightRequestProperty, Value = 40 },
new Setter { Property = WidthRequestProperty, Value = 40 },
new Setter { Property = Button.FontAttributesProperty, Value = FontAttributes.Bold }
}
};
Resources = new ResourceDictionary();
Resources.Add("unSelectedStyle", unselectedStyle);
Resources.Add("selectedStyle", selectedStyle);
}
}
Or you could use Xamarin.Forms.StepProgressBar. Install it from NuGet.
How can I make a Form in which the elements are automatically divided into sections based on their first letter and add to the right the alphabet jumper to show the elements starting by the selected letter (just like the Contacts app)?
I also noted a strange thing that I have no idea how to recreate: not all letters are shown, some of them appear as "•". However, when you tap on them, they take you to the corresponding letter anyway. I tried using a ScrollView(.vertical) inside a ZStack and adding .scrollTo(selection) into the action of the Buttons, however 1) It didn't scroll to the selection I wanted 2) When I tapped on the "•", it was as if I was tapping on all of them because they all did the tapping animation 3) I wasn't able to divide the List as I wanted to.
I have this:
import SwiftUI
struct ContentView: View {
let alphabet = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W", "X","Y", "Z"]
let values = ["Avalue", "Bvalue", "Cvalue", "Dvalue"]
var body: some View {
ScrollViewReader{ scrollviewr in
ZStack {
ScrollView(.vertical) {
VStack {
ForEach(alphabet, id: \.self) { letters in
Button(letters){
withAnimation {
scrollviewr.scrollTo(letters)
}
}
}
}
}.offset(x: 180, y: 120)
VStack {
ForEach(values, id: \.self){ vals in
Text(vals).id(vals)
}
}
}
}
}
}
But I'd want it like this:
import SwiftUI
struct AlphabetSort2: View {
let alphabet = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W", "X","Y", "Z"]
let values = ["Avalue", "Bvalue", "Cvalue", "Dvalue", "Mvalue", "Zvalue"]
var body: some View {
ScrollView {
ScrollViewReader { value in
ZStack{
List{
ForEach(alphabet, id: \.self) { letter in
Section(header: Text(letter)) {
ForEach(values.filter { $0.hasPrefix(letter) }, id: \.self) { vals in
Text(vals).id(vals)
}
}.id(letter)
}
}
HStack{
Spacer()
VStack {
ForEach(0..<alphabet.count, id: \.self) { idx in
Button(action: {
withAnimation {
value.scrollTo(alphabet[idx])
}
}, label: {
Text(idx % 2 == 0 ? alphabet[idx] : "\u{2022}")
})
}
}
}
}
}
}
}
}
struct AlphabetSort2_Previews: PreviewProvider {
static var previews: some View {
AlphabetSort2()
}
}
Add an swipe-up-and-down action for the alphabet jumper, so we can get an UILocalizedIndexCollation -like swipe effect, it works for view and add mode, but delete, I guess it is due to SwiftUI's UI refresh mechanism.
extension String {
static var alphabeta: [String] {
var chars = [String]()
for char in "abcdefghijklmnopqrstuvwxyz#".uppercased() {
chars.append(String(char))
}
return chars
}
}
struct Document: View {
#State var items = ["Alpha", "Ash", "Aman", "Alisia", "Beta", "Baum", "Bob", "Bike", "Beeber", "Beff", "Calipha", "Cask", "Calf", "Deamon", "Deaf", "Dog", "Silk", "Seal", "Tiger", "Tom", "Tan", "Tint", "Urshinabi", "Verizon", "Viber", "Vein", "Wallet", "Warren", "Webber", "Waiter", "Xeon", "Young", "Yoda", "Yoga", "Yoger", "Yellow", "Zeta"]
var body: some View {
ScrollViewReader { scrollView in
HStack {
List {
ForEach(String.alphabeta, id: \.self){ alpha in
let subItems = items.filter({$0.starts(with: alpha)})
if !subItems.isEmpty {
Section(header: Text(alpha)) {
ForEach(subItems, id: \.self) { item in
Text(item)
}.onDelete(perform: { offsets in
items.remove(at: offsets.first!)
})
}.id(alpha)
}
}
}
VStack{
VStack {
SectionIndexTitles(proxy: scrollView, titles: retrieveSectionTitles()).font(.footnote)
}
.padding(.trailing, 10)
}
}
}
// .navigationBarTitleDisplayMode(.inline)
// .navigationBarHidden(true)
}
func retrieveSectionTitles() ->[String] {
var titles = [String]()
titles.append("#")
for item in self.items {
if !item.starts(with: titles.last!){
titles.append(String(item.first!))
}
}
titles.remove(at: 0)
if titles.count>1 && titles.first! == "#" {
titles.append("#")
titles.removeFirst(1)
}
return titles
}
}
struct Document_Previews: PreviewProvider {
static var previews: some View {
Document()
}
}
struct SectionIndexTitles: View {
class IndexTitleState: ObservableObject {
var currentTitleIndex = 0
var titleSize: CGSize = .zero
}
let proxy: ScrollViewProxy
let titles: [String]
#GestureState private var dragLocation: CGPoint = .zero
#StateObject var indexState = IndexTitleState()
var body: some View {
VStack {
ForEach(titles, id: \.self) { title in
Text(title)
.foregroundColor(.blue)
.modifier(SizeModifier())
.onPreferenceChange(SizePreferenceKey.self) {
self.indexState.titleSize = $0
}
.onTapGesture {
proxy.scrollTo(title, anchor: .top)
}
}
}
.gesture(
DragGesture(minimumDistance: indexState.titleSize.height, coordinateSpace: .named(titles.first))
.updating($dragLocation) { value, state, _ in
state = value.location
scrollTo(location: state)
}
)
}
private func scrollTo(location: CGPoint){
if self.indexState.titleSize.height > 0{
let index = Int(location.y / self.indexState.titleSize.height)
if index >= 0 && index < titles.count {
if indexState.currentTitleIndex != index {
indexState.currentTitleIndex = index
print(titles[index])
DispatchQueue.main.async {
let impactMed = UIImpactFeedbackGenerator(style: .medium)
impactMed.impactOccurred()
// withAnimation {
proxy.scrollTo(titles[indexState.currentTitleIndex], anchor: .top)
// }
}
}
}
}
}
}
struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
struct SizeModifier: ViewModifier {
private var sizeView: some View {
GeometryReader { geometry in
Color.clear.preference(key: SizePreferenceKey.self, value: geometry.size)
}
}
func body(content: Content) -> some View {
content.background(sizeView)
}
}
I am working with Contacts via REST API and I am trying to add an input field in my application for specifying the Source of the Contact. The problem is that the Source field is a combo box, which means that its values can be modified by Automation Steps (SM205000). For example, below is the list of the default values for the Source:
which corresponds to the CRMSourcesAttribute of that field
// Token: 0x020003D3 RID: 979
public class CRMSourcesAttribute : PXStringListAttribute
{
// Token: 0x06004052 RID: 16466 RVA: 0x000FD4A4 File Offset: 0x000FB6A4
public CRMSourcesAttribute() : base(new string[]
{
"W",
"H",
"R",
"L",
"O"
}, new string[]
{
"Web",
"Phone Inquiry",
"Referral",
"Purchased List",
"Other"
})
{
}
// Token: 0x04002158 RID: 8536
public const string _WEB = "W";
// Token: 0x04002159 RID: 8537
public const string _PHONE_INQ = "H";
// Token: 0x0400215A RID: 8538
public const string _REFERRAL = "R";
// Token: 0x0400215B RID: 8539
public const string _PURCHASED_LIST = "L";
// Token: 0x0400215C RID: 8540
public const string _OTHER = "O";
}
Should I go through the Automation Steps tables to get the final values of the combo-box or there is a way to get it by REST API specifying, for example, the DAC.FIELD?
For tasks like these, I'd suggest to use reflection. Below goes example of reading Attributes with sample of usage:
protected IEnumerable records()
{
//var row = new ClassFilter { ClassName = "PX.Objects.CR.CRMSourcesAttribute" };
var row = Filter.Current;
if (row != null && !string.IsNullOrWhiteSpace(row.ClassName))
{
var type = Type.GetType(row.ClassName) ??
Type.GetType(row.ClassName + ", PX.Objects");
if (type != null)
switch (type.BaseType.Name)
{
case "PXIntListAttribute":
{
int[] values;
string[] labels;
GetRecords(type, out values, out labels);
for (int i = 0; i < values.Length; i++)
yield return new KeyValueRecord { Key = values[i].ToString(), UiValue = labels[i] };
break;
}
case "PXStringListAttribute":
{
string[] values, labels;
GetRecords(type, out values, out labels);
for (int i = 0; i < values.Length; i++)
yield return new KeyValueRecord { Key = values[i], UiValue = labels[i] };
break;
}
}
}
}
private void GetRecords<T>(Type type, out T[] values, out string[] labels)
{
var obj = Activator.CreateInstance(type);
var flags = BindingFlags.NonPublic | BindingFlags.Instance;
values = type.GetField("_AllowedValues", flags).GetValue(obj) as T[];
labels = type.GetField("_AllowedLabels", flags).GetValue(obj) as string[];
}
and picture:
After that you can just add graph and DAC to endpoint and expose it.
Full source code with comments is available here.
I am using normal mvc textboxfor on strong view, I have created custom validation attribute (the detailed code explained below).
On form submit everything works fine. In case if validation fails by natural it shows error message as configured.
Now when I enter the correct value inside text box I expect the error message to be vanished automatically but this does not happen until I post the form
JS File
$.validator.addMethod('validaterequiredif', function (value, element, parameters) {
var id = parameters['dependentproperty'];
var clickValue = $("input[name=" + id + "]:checked").val();
// get the target value (as a string,
// as that's what actual value will be)
var targetvalue = parameters['targetvalue'];
if (clickValue == targetvalue) {
if (value == null) {
return false;
}
else {
return $.validator.methods.required.call(
this, value, element, parameters);
}
}
else {
return true;
}
});
$.validator.unobtrusive.adapters.add(
'validaterequiredif',
['dependentproperty', 'targetvalue'],
function (options) {
options.rules['validaterequiredif'] = {
dependentproperty: options.params['dependentproperty'],
targetvalue: options.params['targetvalue']
};
options.messages['validaterequiredif'] = options.message;
});
Server side custom validator class as below
public class ValidateRequiredIf : ValidationAttribute, IClientValidatable
{
protected RequiredAttribute _innerAttribute;
public string DependentProperty { get; set; }
public object TargetValue { get; set; }
public bool AllowEmptyStrings
{
get
{
return _innerAttribute.AllowEmptyStrings;
}
set
{
_innerAttribute.AllowEmptyStrings = value;
}
}
public ValidateRequiredIf(string dependentProperty, object targetValue)
{
_innerAttribute = new RequiredAttribute();
DependentProperty = dependentProperty;
TargetValue = targetValue;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
// get a reference to the property this validation depends upon
var containerType = validationContext.ObjectInstance.GetType();
var field = containerType.GetProperty(DependentProperty);
if (field != null)
{
// get the value of the dependent property
var dependentValue = field.GetValue(validationContext.ObjectInstance, null);
// trim spaces of dependent value
if (dependentValue != null && dependentValue is string)
{
dependentValue = (dependentValue as string).Trim();
if (!AllowEmptyStrings && (dependentValue as string).Length == 0)
{
dependentValue = null;
}
}
// compare the value against the target value
if ((dependentValue == null && TargetValue == null) ||
(dependentValue != null && (TargetValue.Equals("*") || dependentValue.Equals(TargetValue))))
{
// match => means we should try validating this field
//if (!_innerAttribute.IsValid(value))
if (value == null)
// validation failed - return an error
return new ValidationResult(FormatErrorMessage(validationContext.DisplayName), new[] { validationContext.MemberName });
}
}
return ValidationResult.Success;
}
private string BuildDependentPropertyId(ModelMetadata metadata, ViewContext viewContext)
{
// build the ID of the property
string depProp = viewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(DependentProperty);
// unfortunately this will have the name of the current field appended to the beginning,
// because the TemplateInfo's context has had this fieldname appended to it. Instead, we
// want to get the context as though it was one level higher (i.e. outside the current property,
// which is the containing object, and hence the same level as the dependent property.
var thisField = metadata.PropertyName + "_";
if (depProp.StartsWith(thisField))
// strip it off again
depProp = depProp.Substring(thisField.Length);
return depProp;
}
public virtual IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var rule = new ModelClientValidationRule
{
ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()),
ValidationType = "validaterequiredif",
};
string depProp = BuildDependentPropertyId(metadata, context as ViewContext);
// find the value on the control we depend on;
// if it's a bool, format it javascript style
// (the default is True or False!)
string targetValue = (TargetValue ?? "").ToString();
if (TargetValue is bool)
targetValue = targetValue.ToLower();
rule.ValidationParameters.Add("dependentproperty", depProp);
rule.ValidationParameters.Add("targetvalue", targetValue);
yield return rule;
}
}
Model property
[ValidateRequiredIf("IsFeederSelected", "True", ErrorMessage = "Please select atleast one feeder")]
public List<string> selectedMeterName { get; set; }
Strongly typed view
<div class="meterTextboxRadio col-md-4">
<p>
#Html.RadioButtonFor(m => m.IsFeederSelected, true, new { #class = "radio", #Name = "IsFeederSelected", value = "meter", id = "rdbMeterConsumption", #checked = "checked" })
<span> Feeder</span>
#Html.RadioButtonFor(m => m.IsFeederSelected, false, new { #class = "radio", #Name = "IsFeederSelected", value = "group", id = "rdbGroupConsumption",style= "margin-left: 30px;" })
<span> Group</span>
</p>
<div class="group dropdownhidden" id="MeterNameConsumption" style="margin-top:4px;">
#Html.DropDownListFor(m => m.selectedMeterName, Model.MeterName
, new { #class = "chosen-select" ,#id= "ddlConsumptionMeterName", multiple = "multiple", Style = "width:100%", data_placeholder = "Choose Feeders.." })
<span class="highlight"></span> <span class="bar"></span>
</div>
<div class="group dropdownhidden" id="GroupNameConsumption" style="margin-top:4px;">
#Html.DropDownListFor(m => m.selectedGroupName, Model.GroupName
, new { #class = "chosen-select", #id = "ddlConsumptionGroupName", multiple = "multiple", Style = "width:100%", data_placeholder = "Choose Group.." })
<span class="highlight"></span> <span class="bar"></span>
</div>
#Html.ValidationMessageFor(m => m.selectedMeterName, "", new { #class = "error" })
#Html.ValidationMessageFor(m => m.selectedGroupName, "", new { #class = "error" })
</div>
Please provide some inputs for the same
Thanks.