@ -0,0 +1,297 @@ |
|||||
|
# Using Dapper with the ABP Framework |
||||
|
|
||||
|
[Dapper](https://github.com/DapperLib/Dapper) is a simple and lightweight object mapper for .NET. A key feature of Dapper is its [high performance](https://github.com/DapperLib/Dapper#performance) compared to other ORMs. In this article, I will show how to use it in your ABP projects. But, first look at when to use it. |
||||
|
|
||||
|
### Source Code |
||||
|
|
||||
|
You can find the [full source code of the demo application here](https://github.com/abpframework/abp-samples/tree/master/Dapper). |
||||
|
|
||||
|
## When to use Dapper? |
||||
|
|
||||
|
In ABP Framework, we suggest to use Dapper in a combination with Entity Framework Core (EF Core) for the following reasons: |
||||
|
|
||||
|
* EF Core is much easier to use (you don't need to manually write SQL queries and work with low level database objects). |
||||
|
* EF Core abstracts different DBMS dialects, so it will be easier to change your DBMS later. |
||||
|
* EF Core's change tracking system automatically update changes in the database. |
||||
|
* EF Core is better compatible with Object Oriented Programming (OOP) practices and is more type safe to work with. So, EF Core code is more understandable and maintainable. |
||||
|
|
||||
|
In most of your use cases, you typically work with one or a few entities and a maintainable codebase can be chosen instead of a slight performance difference. However, there may be certain places in your application where it matters: |
||||
|
|
||||
|
* You may work with a lot of entities, so want to query faster (Indeed, EF Core's [AsNoTracking()](https://learn.microsoft.com/en-us/ef/core/querying/tracking) extension can help in most cases). |
||||
|
* You may be performing too many database operations in a single request. |
||||
|
* EF Core may not create an optimized SQL query and you may want to manually write it for a better performance. |
||||
|
|
||||
|
For such cases, Dapper can be a good choice. You can easily write SQL queries and bind the result to your objects. |
||||
|
|
||||
|
## Creating a new ABP Solution |
||||
|
|
||||
|
To demonstrate the Dapper usage, I've created an ABP solution. You can find the [full source code of the demo application here](https://github.com/abpframework/abp-samples/tree/master/Dapper). If you want to create the same solution from scratch, follow the steps below: |
||||
|
|
||||
|
Install the ABP CLI if you haven't installed it before: |
||||
|
|
||||
|
````bash |
||||
|
dotnet tool install -g Volo.Abp.Cli |
||||
|
```` |
||||
|
|
||||
|
Create a new solution with the ABP Framework's non-layered startup template with MVC UI and EF Core database: |
||||
|
|
||||
|
````bash |
||||
|
abp new DapperDemo -t app-nolayers |
||||
|
```` |
||||
|
|
||||
|
> The startup template or a different UI selection don't matter for this article. I selected these options to keep the demo solution simple. |
||||
|
|
||||
|
After creating the solution, run the following command to migrate the database (run the command in the folder of the `.csproj` file: |
||||
|
|
||||
|
````csharp |
||||
|
dotnet run --migrate-database |
||||
|
```` |
||||
|
|
||||
|
> If you've created a layered solution, then run the `DbMigrator` application inside the solution. If you have trouble by creating the solution, please refer to the [Quick Start](https://docs.abp.io/en/abp/latest/Tutorials/Todo/Single-Layer/Index) document. |
||||
|
|
||||
|
## Setting Up the Entity Framework Core Part |
||||
|
|
||||
|
We will use EF Core with Dapper, so we need to configure the EF Core first. I will use the following `Book` entity as an example: |
||||
|
|
||||
|
````csharp |
||||
|
public class Book : AuditedAggregateRoot<Guid> |
||||
|
{ |
||||
|
public string Name { get; set; } |
||||
|
public DateTime PublishDate { get; set; } |
||||
|
public float Price { get; set; } |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
If you are using a layered solution, entities are located in the `Domain` project. For my demo solution, I just placed it in the `Entities` folder of the single project: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
Once I created the `Book` entity, I should add it to my `DbContext` class: |
||||
|
|
||||
|
````csharp |
||||
|
public class DapperDemoDbContext : AbpDbContext<DapperDemoDbContext> |
||||
|
{ |
||||
|
// 1: ADD A DBSET PROPERTY |
||||
|
public DbSet<Book> Books { get; set; } |
||||
|
|
||||
|
public DapperDemoDbContext(DbContextOptions<DapperDemoDbContext> options) |
||||
|
: base(options) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
protected override void OnModelCreating(ModelBuilder builder) |
||||
|
{ |
||||
|
//...other code parts |
||||
|
|
||||
|
// 2: MAP YOUR ENTITY TO A DATABASE TABLE |
||||
|
builder.Entity<Book>(b => |
||||
|
{ |
||||
|
b.ToTable("Books"); |
||||
|
b.Property(x => x.Name).IsRequired().HasMaxLength(128); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
```` |
||||
|
|
||||
|
Now, we can add a new database migration: |
||||
|
|
||||
|
````bash |
||||
|
dotnet ef migrations add Added_Book |
||||
|
```` |
||||
|
|
||||
|
And apply changes to database: |
||||
|
|
||||
|
````bash |
||||
|
dotnet ef database update |
||||
|
```` |
||||
|
|
||||
|
At this point, you should see the Books table if you check your database: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
> As you see, the `Books` table contains more fields than the `Book` entity's property count. Other properties are inherited from the `AuditedAggregateRoot` table. You could inherit from the `BasicAggregateRoot` class if you don't want these properties. |
||||
|
|
||||
|
## Seeding the Database |
||||
|
|
||||
|
ABP's [data seeding](https://docs.abp.io/en/abp/latest/Data-Seeding) system is a great way to add some test data to the database. The following class inserts two books to the `Books` table when I migrate the database: |
||||
|
|
||||
|
````csharp |
||||
|
public class DapperDemoDataSeederContributor : IDataSeedContributor, ITransientDependency |
||||
|
{ |
||||
|
private readonly IRepository<Book, Guid> _bookRepository; |
||||
|
|
||||
|
public DapperDemoDataSeederContributor(IRepository<Book, Guid> bookRepository) |
||||
|
{ |
||||
|
_bookRepository = bookRepository; |
||||
|
} |
||||
|
|
||||
|
public async Task SeedAsync(DataSeedContext context) |
||||
|
{ |
||||
|
if (await _bookRepository.GetCountAsync() > 0) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
await _bookRepository.InsertAsync( |
||||
|
new Book |
||||
|
{ |
||||
|
Name = "1984", |
||||
|
PublishDate = new DateTime(1949, 6, 8), |
||||
|
Price = 19.84f |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
await _bookRepository.InsertAsync( |
||||
|
new Book |
||||
|
{ |
||||
|
Name = "The Hitchhiker's Guide to the Galaxy", |
||||
|
PublishDate = new DateTime(1995, 9, 27), |
||||
|
Price = 42.0f |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
After creating the `DapperDemoDataSeederContributor` class, I can re-run the following command: |
||||
|
|
||||
|
````bash |
||||
|
dotnet run --migrate-database |
||||
|
```` |
||||
|
|
||||
|
Now, I can see the records in the database: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
Now, all ready to try Dapper to query from the `Books` table. |
||||
|
|
||||
|
## Using Dapper Without the Integration Package |
||||
|
|
||||
|
ABP provides an integration package for Dapper. However, first I want to show using Dapper without the integration package. |
||||
|
|
||||
|
### Installing the Dapper Package |
||||
|
|
||||
|
First, install the [Dapper](https://www.nuget.org/packages/Dapper) package to your project. You can use a command-line terminal, locate the root path of your project (`.csproj` file that you want to install it) and run the following command: |
||||
|
|
||||
|
````bash |
||||
|
dotnet add package Dapper |
||||
|
```` |
||||
|
|
||||
|
> If your application is layered, then we suggest to add the `Dapper` package to your `EntityFrameworkCore` integration project in your solution. |
||||
|
|
||||
|
### Executing a Dapper Query |
||||
|
|
||||
|
I will query from the `Books` table, but I don't want to use the `Book` entity to map the result (because I don't need to all properties). So, I am creating a new class for the query result: |
||||
|
|
||||
|
````csharp |
||||
|
public class BookDataView |
||||
|
{ |
||||
|
public Guid Id { get; set; } |
||||
|
public string Name { get; set; } |
||||
|
public float Price { get; set; } |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
Now, we can use Dapper's `QueryAsync` extension method as shown below: |
||||
|
|
||||
|
````csharp |
||||
|
public class DemoService : ITransientDependency |
||||
|
{ |
||||
|
private readonly IRepository<Book, Guid> _bookRepository; |
||||
|
|
||||
|
public DemoService(IRepository<Book, Guid> bookRepository) |
||||
|
{ |
||||
|
_bookRepository = bookRepository; |
||||
|
} |
||||
|
|
||||
|
[UnitOfWork] |
||||
|
public virtual async Task<List<BookDataView>> GetListAsync() |
||||
|
{ |
||||
|
var database = (await _bookRepository.GetDbContextAsync()).Database; |
||||
|
var dbConnection = database.GetDbConnection(); |
||||
|
var dbTransaction = database.CurrentTransaction?.GetDbTransaction(); |
||||
|
|
||||
|
var queryResult = await dbConnection.QueryAsync<BookDataView>( |
||||
|
"SELECT Id, Name, Price FROM Books", |
||||
|
transaction: dbTransaction |
||||
|
); |
||||
|
|
||||
|
return queryResult.ToList(); |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
Let's examine that class: |
||||
|
|
||||
|
* I am injecting ABP's standard [generic repository](https://docs.abp.io/en/abp/latest/Repositories) service in to the `DemoService` constructor. |
||||
|
* `_bookRepository.GetDbContextAsync()` returns the underlying `DbContext` object of EF Core. We need to have the [Volo.Abp.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore) package reference to be able to access that method. If you have created a single-layer solution, the reference does already exists. If you have created a layered solution you may need to manually add this package to the project that contains the `DemoService` class. Because the layered solution isolates the EF Core dependency from rest of the solution. |
||||
|
* Dapper needs a `DbConnection` and a `DbTransaction` object (as optional) to execute a query. We are getting them over the `database` object obtained from the `DbContext`. We suggest to always pass the current `DbTransaction` object while working with Dapper. Because, if there is a database transaction on the same database connection that you execute queries, and you don't pass the transaction object, you get an exception. |
||||
|
* Finally, we can use Dapper's `QueryAsync` extension method to execute the database query. |
||||
|
* Notice that the `GetListAsync` method is made as `virtual` and marked with `UnitOfWork` attribute to enable the [Unit Of Work](https://docs.abp.io/en/abp/latest/Unit-Of-Work) for that method. It ensures the database connection is available in the body of the `GetListAsync` method. |
||||
|
|
||||
|
That's all. You can execute any Dapper operation using the `DbConnection` and `DbTransaction` objects obtained from the `_bookRepository` object. Please refer to Dapper's documentation for other operations. |
||||
|
|
||||
|
> We've obtained the `DbContext` object from a repository. However, a repository is not required to obtain a `DbContext`. Instead, you could inject the `IDbContextProvider<T>` service (`IDbContextProvider<DapperDemoDbContext>` for this demo) and call its `GetDbContextAsync` method. |
||||
|
|
||||
|
## Using the Volo.Abp.Dapper Package |
||||
|
|
||||
|
In the previous section, you saw that you don't need to an ABP integration package to be able to use Dapper in your ABP applications. However, there is an integration package here: [Volo.Abp.Dapper](https://www.nuget.org/packages/Volo.Abp.Dapper). Actually, that package doesn't contain much services. It just provides a convenient base class to create Dapper based repository classes. |
||||
|
|
||||
|
### Installing the Volo.Abp.Dapper Package |
||||
|
|
||||
|
You can use [ABP CLI](https://docs.abp.io/en/abp/latest/CLI#add-package) to easily install ABP packages to your projects. Execute the following command in the folder of `.csproj` you want to install the package: |
||||
|
|
||||
|
```` |
||||
|
abp add-package Volo.Abp.Dapper |
||||
|
```` |
||||
|
|
||||
|
> You can check the [ABP Dapper document](https://docs.abp.io/en/abp/latest/Dapper) for alternative installation options. |
||||
|
|
||||
|
### Creating a Repository Class |
||||
|
|
||||
|
In the `DemoService` example, we have used database objects out of a [repository](https://docs.abp.io/en/abp/latest/Repositories) class. If you want to implement layering your solution and abstracting database operations, it can be better to create a repository class to execute Dapper operations. |
||||
|
|
||||
|
Here, a repository class that executes the same database query: |
||||
|
|
||||
|
````csharp |
||||
|
public class BookRepository : DapperRepository<DapperDemoDbContext>, ITransientDependency |
||||
|
{ |
||||
|
public BookRepository(IDbContextProvider<DapperDemoDbContext> dbContextProvider) |
||||
|
: base(dbContextProvider) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public virtual async Task<List<BookDataView>> GetListAsync() |
||||
|
{ |
||||
|
var connection = await GetDbConnectionAsync(); |
||||
|
var queryResult = await connection.QueryAsync<BookDataView>( |
||||
|
"SELECT Id, Name, Price FROM Books", |
||||
|
transaction: await GetDbTransactionAsync() |
||||
|
); |
||||
|
return queryResult.ToList(); |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
Let's examine that class: |
||||
|
|
||||
|
* It inherits from the `DapperRepository` class, which provides useful methods and properties for database operations. It also implements the `IUnitOfWorkEnabled` interface, so ABP makes the database connection (and transaction if requested) available in the method body by implementing dynamic proxies (a.k.a. interception). |
||||
|
* `GetListAsync` method made `virtual`. That's needed to make the interception process working. It wouldn't be needed if we introduce `IBookRepository` for that class and always use it by injecting the `BookRepository` class (in this case, it will use interface proxying - however, this is too much details for the purpose of this article). |
||||
|
* We are using the `GetDbConnectionAsync` and `GetDbTransactionAsync` methods to obtain the current database connection and transaction (that is managed by ABP's [unit of work](https://docs.abp.io/en/abp/latest/Unit-Of-Work) system). |
||||
|
|
||||
|
You can then inject the `BookRepository` class when you want to get a list of `BookDataView` whenever it is needed. In the demo project, I used it inside the `IndexModel.cshtml.cs` to show a list of books on the page: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## Conclusion |
||||
|
|
||||
|
In this article, I've explained ABP's Dapper integration and demonstrated how you can execute Dapper operations in your applications. I suggest to use Dapper when it is really needed and adds any significant value (generally a performance gain) to your application. Otherwise, EF Core is much more convenient for most of the database operations and you will have a more maintainable codebase using EF Core. |
||||
|
|
||||
|
## Source Code |
||||
|
|
||||
|
You can find the [full source code of the demo application here](https://github.com/abpframework/abp-samples/tree/master/Dapper). |
||||
|
|
||||
|
## See Also |
||||
|
|
||||
|
* [ABP Dapper documentation](https://docs.abp.io/en/abp/latest/Dapper) |
||||
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
@ -0,0 +1,372 @@ |
|||||
|
# Convert Create/Edit Modals to Page |
||||
|
In this document we will explain how to convert BookStore's Books create & edit modals to regular blazor pages. |
||||
|
|
||||
|
## Before |
||||
|
 |
||||
|
|
||||
|
## After |
||||
|
|
||||
|
 |
||||
|
|
||||
|
|
||||
|
# Books.razor Page |
||||
|
Books.razor page is the main page of the books management. Create & Update operations are done in this page. So we'll remove create & update operations from this page and move a separate blazor component for each operation. Each component will be a page. |
||||
|
|
||||
|
- Remove both Create & Update modals. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
- Replace **NewBook** button with a link to **CreateBook** page. |
||||
|
|
||||
|
```html |
||||
|
<Button Color="Color.Primary" Type="ButtonType.Link" To="books/new"> |
||||
|
@L["NewBook"] |
||||
|
</Button> |
||||
|
``` |
||||
|
|
||||
|
- Inject `NavigationManager` to `Books.razor` page. |
||||
|
```csharp |
||||
|
@inject NavigationManager NavigationManager |
||||
|
``` |
||||
|
|
||||
|
- Replace **Edit** button with a link to **UpdateBook** page. |
||||
|
|
||||
|
```html |
||||
|
<Button Color="Color.Primary" Type="ButtonType.Link" OnClick="() => NavigateToEdit(book.Id)"> |
||||
|
@L["Edit"] |
||||
|
</Button> |
||||
|
``` |
||||
|
|
||||
|
```csharp |
||||
|
private void NavigateToEdit(Guid id) |
||||
|
{ |
||||
|
NavigationManager.NavigateTo($"books/{id}/edit"); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
- Remove all methods in the `Books.razor` page except constructor. And add `GoToEditPage` as below: |
||||
|
|
||||
|
```csharp |
||||
|
protected void GoToEditPage(BookDto book) |
||||
|
{ |
||||
|
NavigationManager.NavigateTo($"books/{book.Id}"); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
 |
||||
|
|
||||
|
- Change Edit button to a link in the table. |
||||
|
|
||||
|
```html |
||||
|
<EntityAction TItem="BookDto" |
||||
|
Text="@L["Edit"]" |
||||
|
Visible=HasUpdatePermission |
||||
|
Clicked="() => GoToEditPage(context)" /> |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
# CreateBooks Page |
||||
|
Create new `CreateBook.razor` and `CreateBook.razor.cs` files in your project. |
||||
|
|
||||
|
- `CreateBook.razor` |
||||
|
|
||||
|
```html |
||||
|
@page "/books/new" |
||||
|
@attribute [Authorize(BookStorePermissions.Books.Create)] |
||||
|
@inherits BookStoreComponentBase |
||||
|
|
||||
|
@using Acme.BookStore.Books; |
||||
|
@using Acme.BookStore.Localization; |
||||
|
@using Acme.BookStore.Permissions; |
||||
|
@using Microsoft.Extensions.Localization; |
||||
|
@using Volo.Abp.AspNetCore.Components.Web; |
||||
|
|
||||
|
@inject IStringLocalizer<BookStoreResource> L |
||||
|
@inject AbpBlazorMessageLocalizerHelper<BookStoreResource> LH |
||||
|
@inject IBookAppService AppService |
||||
|
@inject NavigationManager NavigationManager |
||||
|
|
||||
|
<Card> |
||||
|
<CardHeader> |
||||
|
<HeadContent> |
||||
|
<ModalTitle>@L["NewBook"]</ModalTitle> |
||||
|
</HeadContent> |
||||
|
</CardHeader> |
||||
|
<CardBody> |
||||
|
<Validations @ref="@CreateValidationsRef" Model="@NewEntity" ValidateOnLoad="false"> |
||||
|
<Validation MessageLocalizer="@LH.Localize"> |
||||
|
<Field> |
||||
|
<FieldLabel>@L["Author"]</FieldLabel> |
||||
|
<Select TValue="Guid" @bind-SelectedValue="@NewEntity.AuthorId"> |
||||
|
@foreach (var author in authorList) |
||||
|
{ |
||||
|
<SelectItem TValue="Guid" Value="@author.Id"> |
||||
|
@author.Name |
||||
|
</SelectItem> |
||||
|
} |
||||
|
</Select> |
||||
|
</Field> |
||||
|
<Field> |
||||
|
<FieldLabel>@L["Name"]</FieldLabel> |
||||
|
<TextEdit @bind-Text="@NewEntity.Name"> |
||||
|
<Feedback> |
||||
|
<ValidationError /> |
||||
|
</Feedback> |
||||
|
</TextEdit> |
||||
|
</Field> |
||||
|
</Validation> |
||||
|
<Field> |
||||
|
<FieldLabel>@L["Type"]</FieldLabel> |
||||
|
<Select TValue="BookType" @bind-SelectedValue="@NewEntity.Type"> |
||||
|
@foreach (int bookTypeValue in Enum.GetValues(typeof(BookType))) |
||||
|
{ |
||||
|
<SelectItem TValue="BookType" Value="@((BookType) bookTypeValue)"> |
||||
|
@L[$"Enum:BookType.{bookTypeValue}"] |
||||
|
</SelectItem> |
||||
|
} |
||||
|
</Select> |
||||
|
</Field> |
||||
|
<Field> |
||||
|
<FieldLabel>@L["PublishDate"]</FieldLabel> |
||||
|
<DateEdit TValue="DateTime" @bind-Date="NewEntity.PublishDate" /> |
||||
|
</Field> |
||||
|
<Field> |
||||
|
<FieldLabel>@L["Price"]</FieldLabel> |
||||
|
<NumericEdit TValue="float" @bind-Value="NewEntity.Price" /> |
||||
|
</Field> |
||||
|
</Validations> |
||||
|
</CardBody> |
||||
|
<CardFooter> |
||||
|
<Button Color="Color.Secondary" Type="ButtonType.Link" To="books"> |
||||
|
@L["Cancel"] |
||||
|
</Button> |
||||
|
<Button Color="Color.Primary" |
||||
|
Type="@ButtonType.Submit" |
||||
|
PreventDefaultOnSubmit="true" |
||||
|
Clicked="CreateEntityAsync"> |
||||
|
@L["Save"] |
||||
|
</Button> |
||||
|
</CardFooter> |
||||
|
</Card> |
||||
|
``` |
||||
|
|
||||
|
- `CreateBook.razor.cs` |
||||
|
|
||||
|
```csharp |
||||
|
using Acme.BookStore.Books; |
||||
|
using Blazorise; |
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp; |
||||
|
|
||||
|
namespace Acme.BookStore.Blazor.Pages; |
||||
|
|
||||
|
public partial class CreateBook |
||||
|
{ |
||||
|
protected Validations CreateValidationsRef; |
||||
|
protected CreateUpdateBookDto NewEntity = new(); |
||||
|
IReadOnlyList<AuthorLookupDto> authorList = Array.Empty<AuthorLookupDto>(); |
||||
|
|
||||
|
protected override async Task OnInitializedAsync() |
||||
|
{ |
||||
|
await base.OnInitializedAsync(); |
||||
|
authorList = (await AppService.GetAuthorLookupAsync()).Items; |
||||
|
|
||||
|
if (!authorList.Any()) |
||||
|
{ |
||||
|
throw new UserFriendlyException(message: L["AnAuthorIsRequiredForCreatingBook"]); |
||||
|
} |
||||
|
|
||||
|
NewEntity.AuthorId = authorList.First().Id; |
||||
|
|
||||
|
if (CreateValidationsRef != null) |
||||
|
{ |
||||
|
await CreateValidationsRef.ClearAll(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task CreateEntityAsync() |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var validate = true; |
||||
|
if (CreateValidationsRef != null) |
||||
|
{ |
||||
|
validate = await CreateValidationsRef.ValidateAll(); |
||||
|
} |
||||
|
if (validate) |
||||
|
{ |
||||
|
await AppService.CreateAsync(NewEntity); |
||||
|
NavigationManager.NavigateTo("books"); |
||||
|
} |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
await HandleErrorAsync(ex); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
# EditBooks Page |
||||
|
Create new `EditBook.razor` and `EditBook.razor.cs` files in your project. |
||||
|
|
||||
|
- `EditBook.razor` |
||||
|
|
||||
|
```html |
||||
|
@page "/books/{Id}" |
||||
|
@attribute [Authorize(BookStorePermissions.Books.Edit)] |
||||
|
@inherits BookStoreComponentBase |
||||
|
@using Acme.BookStore.Books; |
||||
|
@using Acme.BookStore.Localization; |
||||
|
@using Acme.BookStore.Permissions; |
||||
|
@using Microsoft.Extensions.Localization; |
||||
|
@using Volo.Abp.AspNetCore.Components.Web; |
||||
|
|
||||
|
@inject IStringLocalizer<BookStoreResource> L |
||||
|
@inject AbpBlazorMessageLocalizerHelper<BookStoreResource> LH |
||||
|
@inject IBookAppService AppService |
||||
|
@inject NavigationManager NavigationManager |
||||
|
|
||||
|
<Card> |
||||
|
<CardHeader> |
||||
|
<HeadContent> |
||||
|
<ModalTitle>@EditingEntity.Name</ModalTitle> |
||||
|
</HeadContent> |
||||
|
</CardHeader> |
||||
|
<CardBody> |
||||
|
<Validations @ref="@EditValidationsRef" Model="@EditingEntity" ValidateOnLoad="false"> |
||||
|
<Validation MessageLocalizer="@LH.Localize"> |
||||
|
<Field> |
||||
|
<FieldLabel>@L["Author"]</FieldLabel> |
||||
|
<Select TValue="Guid" @bind-SelectedValue="@EditingEntity.AuthorId"> |
||||
|
@foreach (var author in authorList) |
||||
|
{ |
||||
|
<SelectItem TValue="Guid" Value="@author.Id"> |
||||
|
@author.Name |
||||
|
</SelectItem> |
||||
|
} |
||||
|
</Select> |
||||
|
</Field> |
||||
|
<Field> |
||||
|
<FieldLabel>@L["Name"]</FieldLabel> |
||||
|
<TextEdit @bind-Text="@EditingEntity.Name"> |
||||
|
<Feedback> |
||||
|
<ValidationError /> |
||||
|
</Feedback> |
||||
|
</TextEdit> |
||||
|
</Field> |
||||
|
</Validation> |
||||
|
<Field> |
||||
|
<FieldLabel>@L["Type"]</FieldLabel> |
||||
|
<Select TValue="BookType" @bind-SelectedValue="@EditingEntity.Type"> |
||||
|
@foreach (int bookTypeValue in Enum.GetValues(typeof(BookType))) |
||||
|
{ |
||||
|
<SelectItem TValue="BookType" Value="@((BookType) bookTypeValue)"> |
||||
|
@L[$"Enum:BookType.{bookTypeValue}"] |
||||
|
</SelectItem> |
||||
|
} |
||||
|
</Select> |
||||
|
</Field> |
||||
|
<Field> |
||||
|
<FieldLabel>@L["PublishDate"]</FieldLabel> |
||||
|
<DateEdit TValue="DateTime" @bind-Date="EditingEntity.PublishDate" /> |
||||
|
</Field> |
||||
|
<Field> |
||||
|
<FieldLabel>@L["Price"]</FieldLabel> |
||||
|
<NumericEdit TValue="float" @bind-Value="EditingEntity.Price" /> |
||||
|
</Field> |
||||
|
</Validations> |
||||
|
</CardBody> |
||||
|
<CardFooter> |
||||
|
<Button Color="Color.Secondary" Type="ButtonType.Link" To="books"> |
||||
|
@L["Cancel"] |
||||
|
</Button> |
||||
|
<Button Color="Color.Primary" |
||||
|
Type="@ButtonType.Submit" |
||||
|
PreventDefaultOnSubmit="true" |
||||
|
Clicked="UpdateEntityAsync"> |
||||
|
@L["Save"] |
||||
|
</Button> |
||||
|
</CardFooter> |
||||
|
</Card> |
||||
|
``` |
||||
|
|
||||
|
- `EditBook.razor.cs` |
||||
|
|
||||
|
```csharp |
||||
|
using Acme.BookStore.Books; |
||||
|
using Blazorise; |
||||
|
using Microsoft.AspNetCore.Components; |
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp; |
||||
|
|
||||
|
namespace Acme.BookStore.Blazor.Pages; |
||||
|
|
||||
|
public partial class EditBook |
||||
|
{ |
||||
|
protected CreateUpdateBookDto EditingEntity = new(); |
||||
|
protected Validations EditValidationsRef; |
||||
|
IReadOnlyList<AuthorLookupDto> authorList = Array.Empty<AuthorLookupDto>(); |
||||
|
|
||||
|
[Parameter] |
||||
|
public string Id { get; set; } |
||||
|
|
||||
|
public Guid EditingEntityId { get; set; } |
||||
|
|
||||
|
protected override async Task OnInitializedAsync() |
||||
|
{ |
||||
|
await base.OnInitializedAsync(); |
||||
|
|
||||
|
// Blazor can't parse Guid as route constraint currently. |
||||
|
// See https://github.com/dotnet/aspnetcore/issues/19008 |
||||
|
EditingEntityId = Guid.Parse(Id); |
||||
|
|
||||
|
authorList = (await AppService.GetAuthorLookupAsync()).Items; |
||||
|
|
||||
|
if (!authorList.Any()) |
||||
|
{ |
||||
|
throw new UserFriendlyException(message: L["AnAuthorIsRequiredForCreatingBook"]); |
||||
|
} |
||||
|
|
||||
|
var entityDto = await AppService.GetAsync(EditingEntityId); |
||||
|
|
||||
|
EditingEntity = ObjectMapper.Map<BookDto,CreateUpdateBookDto>(entityDto); |
||||
|
|
||||
|
if (EditValidationsRef != null) |
||||
|
{ |
||||
|
await EditValidationsRef.ClearAll(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task UpdateEntityAsync() |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var validate = true; |
||||
|
if (EditValidationsRef != null) |
||||
|
{ |
||||
|
validate = await EditValidationsRef.ValidateAll(); |
||||
|
} |
||||
|
if (validate) |
||||
|
{ |
||||
|
await AppService.UpdateAsync(EditingEntityId, EditingEntity); |
||||
|
|
||||
|
NavigationManager.NavigateTo("books"); |
||||
|
} |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
await HandleErrorAsync(ex); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
You can check the following commit for details: |
||||
|
https://github.com/abpframework/abp-samples/commit/aae61ad6d66ebf6191dd4dcfb4e23d30bd680a4e |
||||
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 314 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 202 KiB |
|
After Width: | Height: | Size: 213 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 10 KiB |
@ -0,0 +1,215 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Microsoft.AspNetCore.Razor.TagHelpers; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form.DatePicker; |
||||
|
|
||||
|
public abstract class |
||||
|
AbpDatePickerBaseTagHelper<TTagHelper> : AbpTagHelper<TTagHelper, AbpDatePickerBaseTagHelperService<TTagHelper>>, IAbpDatePickerOptions |
||||
|
where TTagHelper : AbpDatePickerBaseTagHelper<TTagHelper> |
||||
|
|
||||
|
{ |
||||
|
private readonly IAbpDatePickerOptions _abpDatePickerOptionsImplementation; |
||||
|
|
||||
|
public string Label { get; set; } |
||||
|
|
||||
|
public string LabelTooltip { get; set; } |
||||
|
|
||||
|
public string LabelTooltipIcon { get; set; } = "bi-info-circle"; |
||||
|
|
||||
|
public string LabelTooltipPlacement { get; set; } = "right"; |
||||
|
|
||||
|
public bool LabelTooltipHtml { get; set; } = false; |
||||
|
|
||||
|
[HtmlAttributeName("info")] |
||||
|
public string InfoText { get; set; } |
||||
|
|
||||
|
[HtmlAttributeName("disabled")] |
||||
|
public bool IsDisabled { get; set; } = false; |
||||
|
|
||||
|
[HtmlAttributeName("readonly")] |
||||
|
public bool? IsReadonly { get; set; } = false; |
||||
|
|
||||
|
public bool AutoFocus { get; set; } |
||||
|
|
||||
|
public AbpFormControlSize Size { get; set; } = AbpFormControlSize.Default; |
||||
|
|
||||
|
[HtmlAttributeName("required-symbol")] |
||||
|
public bool DisplayRequiredSymbol { get; set; } = true; |
||||
|
|
||||
|
public string Name { get; set; } |
||||
|
|
||||
|
public string Value { get; set; } |
||||
|
|
||||
|
public bool SuppressLabel { get; set; } |
||||
|
|
||||
|
protected AbpDatePickerBaseTagHelper(AbpDatePickerBaseTagHelperService<TTagHelper> service) : base(service) |
||||
|
{ |
||||
|
_abpDatePickerOptionsImplementation = new AbpDatePickerOptions(); |
||||
|
} |
||||
|
|
||||
|
public string PickerId { |
||||
|
get => _abpDatePickerOptionsImplementation.PickerId; |
||||
|
set => _abpDatePickerOptionsImplementation.PickerId = value; |
||||
|
} |
||||
|
|
||||
|
public DateTime? MinDate { |
||||
|
get => _abpDatePickerOptionsImplementation.MinDate; |
||||
|
set => _abpDatePickerOptionsImplementation.MinDate = value; |
||||
|
} |
||||
|
|
||||
|
public DateTime? MaxDate { |
||||
|
get => _abpDatePickerOptionsImplementation.MaxDate; |
||||
|
set => _abpDatePickerOptionsImplementation.MaxDate = value; |
||||
|
} |
||||
|
|
||||
|
public object MaxSpan { |
||||
|
get => _abpDatePickerOptionsImplementation.MaxSpan; |
||||
|
set => _abpDatePickerOptionsImplementation.MaxSpan = value; |
||||
|
} |
||||
|
|
||||
|
public bool? ShowDropdowns { |
||||
|
get => _abpDatePickerOptionsImplementation.ShowDropdowns; |
||||
|
set => _abpDatePickerOptionsImplementation.ShowDropdowns = value; |
||||
|
} |
||||
|
|
||||
|
public int? MinYear { |
||||
|
get => _abpDatePickerOptionsImplementation.MinYear; |
||||
|
set => _abpDatePickerOptionsImplementation.MinYear = value; |
||||
|
} |
||||
|
|
||||
|
public int? MaxYear { |
||||
|
get => _abpDatePickerOptionsImplementation.MaxYear; |
||||
|
set => _abpDatePickerOptionsImplementation.MaxYear = value; |
||||
|
} |
||||
|
|
||||
|
public AbpDatePickerWeekNumbers WeekNumbers { |
||||
|
get => _abpDatePickerOptionsImplementation.WeekNumbers; |
||||
|
set => _abpDatePickerOptionsImplementation.WeekNumbers = value; |
||||
|
} |
||||
|
|
||||
|
public bool? TimePicker { |
||||
|
get => _abpDatePickerOptionsImplementation.TimePicker; |
||||
|
set => _abpDatePickerOptionsImplementation.TimePicker = value; |
||||
|
} |
||||
|
|
||||
|
public int? TimePickerIncrement { |
||||
|
get => _abpDatePickerOptionsImplementation.TimePickerIncrement; |
||||
|
set => _abpDatePickerOptionsImplementation.TimePickerIncrement = value; |
||||
|
} |
||||
|
|
||||
|
public bool? TimePicker24Hour { |
||||
|
get => _abpDatePickerOptionsImplementation.TimePicker24Hour; |
||||
|
set => _abpDatePickerOptionsImplementation.TimePicker24Hour = value; |
||||
|
} |
||||
|
|
||||
|
public bool? TimePickerSeconds { |
||||
|
get => _abpDatePickerOptionsImplementation.TimePickerSeconds; |
||||
|
set => _abpDatePickerOptionsImplementation.TimePickerSeconds = value; |
||||
|
} |
||||
|
|
||||
|
public List<AbpDatePickerRange> Ranges { |
||||
|
get => _abpDatePickerOptionsImplementation.Ranges; |
||||
|
set => _abpDatePickerOptionsImplementation.Ranges = value; |
||||
|
} |
||||
|
|
||||
|
public bool? ShowCustomRangeLabel { |
||||
|
get => _abpDatePickerOptionsImplementation.ShowCustomRangeLabel; |
||||
|
set => _abpDatePickerOptionsImplementation.ShowCustomRangeLabel = value; |
||||
|
} |
||||
|
|
||||
|
public bool? AlwaysShowCalendars { |
||||
|
get => _abpDatePickerOptionsImplementation.AlwaysShowCalendars; |
||||
|
set => _abpDatePickerOptionsImplementation.AlwaysShowCalendars = value; |
||||
|
} |
||||
|
|
||||
|
public AbpDatePickerOpens Opens { |
||||
|
get => _abpDatePickerOptionsImplementation.Opens; |
||||
|
set => _abpDatePickerOptionsImplementation.Opens = value; |
||||
|
} |
||||
|
|
||||
|
public AbpDatePickerDrops Drops { |
||||
|
get => _abpDatePickerOptionsImplementation.Drops; |
||||
|
set => _abpDatePickerOptionsImplementation.Drops = value; |
||||
|
} |
||||
|
|
||||
|
public string ButtonClasses { |
||||
|
get => _abpDatePickerOptionsImplementation.ButtonClasses; |
||||
|
set => _abpDatePickerOptionsImplementation.ButtonClasses = value; |
||||
|
} |
||||
|
|
||||
|
public string TodayButtonClasses { |
||||
|
get => _abpDatePickerOptionsImplementation.TodayButtonClasses; |
||||
|
set => _abpDatePickerOptionsImplementation.TodayButtonClasses = value; |
||||
|
} |
||||
|
|
||||
|
public string ApplyButtonClasses { |
||||
|
get => _abpDatePickerOptionsImplementation.ApplyButtonClasses; |
||||
|
set => _abpDatePickerOptionsImplementation.ApplyButtonClasses = value; |
||||
|
} |
||||
|
|
||||
|
public string ClearButtonClasses { |
||||
|
get => _abpDatePickerOptionsImplementation.ClearButtonClasses; |
||||
|
set => _abpDatePickerOptionsImplementation.ClearButtonClasses = value; |
||||
|
} |
||||
|
|
||||
|
public object Locale { |
||||
|
get => _abpDatePickerOptionsImplementation.Locale; |
||||
|
set => _abpDatePickerOptionsImplementation.Locale = value; |
||||
|
} |
||||
|
|
||||
|
public bool? AutoApply { |
||||
|
get => _abpDatePickerOptionsImplementation.AutoApply; |
||||
|
set => _abpDatePickerOptionsImplementation.AutoApply = value; |
||||
|
} |
||||
|
|
||||
|
public bool? LinkedCalendars { |
||||
|
get => _abpDatePickerOptionsImplementation.LinkedCalendars; |
||||
|
set => _abpDatePickerOptionsImplementation.LinkedCalendars = value; |
||||
|
} |
||||
|
|
||||
|
public bool? AutoUpdateInput { |
||||
|
get => _abpDatePickerOptionsImplementation.AutoUpdateInput; |
||||
|
set => _abpDatePickerOptionsImplementation.AutoUpdateInput = value; |
||||
|
} |
||||
|
|
||||
|
public string ParentEl { |
||||
|
get => _abpDatePickerOptionsImplementation.ParentEl; |
||||
|
set => _abpDatePickerOptionsImplementation.ParentEl = value; |
||||
|
} |
||||
|
|
||||
|
public string DateFormat { |
||||
|
get => _abpDatePickerOptionsImplementation.DateFormat; |
||||
|
set => _abpDatePickerOptionsImplementation.DateFormat = value; |
||||
|
} |
||||
|
|
||||
|
public bool OpenButton { |
||||
|
get => _abpDatePickerOptionsImplementation.OpenButton; |
||||
|
set => _abpDatePickerOptionsImplementation.OpenButton = value; |
||||
|
} |
||||
|
|
||||
|
public bool ClearButton { |
||||
|
get => _abpDatePickerOptionsImplementation.ClearButton; |
||||
|
set => _abpDatePickerOptionsImplementation.ClearButton = value; |
||||
|
} |
||||
|
|
||||
|
public bool SingleOpenAndClearButton { |
||||
|
get => _abpDatePickerOptionsImplementation.SingleOpenAndClearButton; |
||||
|
set => _abpDatePickerOptionsImplementation.SingleOpenAndClearButton = value; |
||||
|
} |
||||
|
|
||||
|
public bool? IsUtc { |
||||
|
get => _abpDatePickerOptionsImplementation.IsUtc; |
||||
|
set => _abpDatePickerOptionsImplementation.IsUtc = value; |
||||
|
} |
||||
|
|
||||
|
public bool? IsIso { |
||||
|
get => _abpDatePickerOptionsImplementation.IsIso; |
||||
|
set => _abpDatePickerOptionsImplementation.IsIso = value; |
||||
|
} |
||||
|
|
||||
|
public object Options { |
||||
|
get => _abpDatePickerOptionsImplementation.Options; |
||||
|
set => _abpDatePickerOptionsImplementation.Options = value; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,712 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.ComponentModel.DataAnnotations; |
||||
|
using System.Linq; |
||||
|
using System.Text.Encodings.Web; |
||||
|
using System.Threading.Tasks; |
||||
|
using JetBrains.Annotations; |
||||
|
using Localization.Resources.AbpUi; |
||||
|
using Microsoft.AspNetCore.Mvc.Rendering; |
||||
|
using Microsoft.AspNetCore.Mvc.TagHelpers; |
||||
|
using Microsoft.AspNetCore.Mvc.ViewFeatures; |
||||
|
using Microsoft.AspNetCore.Razor.TagHelpers; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Microsoft.Extensions.Localization; |
||||
|
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Microsoft.AspNetCore.Razor.TagHelpers; |
||||
|
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Button; |
||||
|
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Extensions; |
||||
|
using Volo.Abp.Json; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form.DatePicker; |
||||
|
|
||||
|
public abstract class AbpDatePickerBaseTagHelperService<TTagHelper> : AbpTagHelperService<TTagHelper> |
||||
|
where TTagHelper : AbpDatePickerBaseTagHelper<TTagHelper> |
||||
|
{ |
||||
|
protected readonly Dictionary<Type,Func<object,string>> SupportedInputTypes = new() { |
||||
|
{typeof(string), o => DateTime.Parse((string)o).ToString("O")}, |
||||
|
{typeof(DateTime), o => ((DateTime) o).ToString("O")}, |
||||
|
{typeof(DateTime?), o => ((DateTime?) o)?.ToString("O")}, |
||||
|
{typeof(DateTimeOffset), o => ((DateTimeOffset) o).ToString("O")}, |
||||
|
{typeof(DateTimeOffset?), o => ((DateTimeOffset?) o)?.ToString("O")} |
||||
|
}; |
||||
|
|
||||
|
protected readonly IJsonSerializer JsonSerializer; |
||||
|
protected readonly IHtmlGenerator Generator; |
||||
|
protected readonly HtmlEncoder Encoder; |
||||
|
protected readonly IServiceProvider ServiceProvider; |
||||
|
protected readonly IAbpTagHelperLocalizer TagHelperLocalizer; |
||||
|
protected virtual string TagName { get; set; } = "abp-date-picker"; |
||||
|
protected IStringLocalizer<AbpUiResource> L { get; } |
||||
|
protected InputTagHelper InputTagHelper { get; set; } |
||||
|
protected abstract TagHelperOutput TagHelperOutput { get; set; } |
||||
|
|
||||
|
protected AbpDatePickerBaseTagHelperService(IJsonSerializer jsonSerializer, IHtmlGenerator generator, |
||||
|
HtmlEncoder encoder, IServiceProvider serviceProvider, IStringLocalizer<AbpUiResource> l, |
||||
|
IAbpTagHelperLocalizer tagHelperLocalizer) |
||||
|
{ |
||||
|
JsonSerializer = jsonSerializer; |
||||
|
Generator = generator; |
||||
|
Encoder = encoder; |
||||
|
ServiceProvider = serviceProvider; |
||||
|
L = l; |
||||
|
TagHelperLocalizer = tagHelperLocalizer; |
||||
|
|
||||
|
InputTagHelper = new InputTagHelper(Generator) { InputTypeName = "text" }; |
||||
|
} |
||||
|
|
||||
|
protected virtual T GetAttribute<T>() where T : Attribute |
||||
|
{ |
||||
|
return GetAttributeAndModelExpression<T>(out _); |
||||
|
} |
||||
|
|
||||
|
protected abstract T GetAttributeAndModelExpression<T>(out ModelExpression modelExpression) where T : Attribute; |
||||
|
|
||||
|
|
||||
|
public async override Task ProcessAsync(TagHelperContext context, TagHelperOutput output) |
||||
|
{ |
||||
|
TagHelperOutput = new TagHelperOutput("input", GetInputAttributes(context, output), (_, _) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent())); |
||||
|
|
||||
|
InputTagHelper.ViewContext = TagHelper.ViewContext; |
||||
|
|
||||
|
if (!TagHelper.Name.IsNullOrEmpty()) |
||||
|
{ |
||||
|
InputTagHelper.Name = TagHelper.Name; |
||||
|
} |
||||
|
|
||||
|
if (!TagHelper.Value.IsNullOrEmpty()) |
||||
|
{ |
||||
|
InputTagHelper.Value = TagHelper.Value; |
||||
|
} |
||||
|
|
||||
|
AddDisabledAttribute(TagHelperOutput); |
||||
|
AddAutoFocusAttribute(TagHelperOutput); |
||||
|
AddFormControls(context, output, TagHelperOutput); |
||||
|
AddReadOnlyAttribute(TagHelperOutput); |
||||
|
AddPlaceholderAttribute(TagHelperOutput); |
||||
|
AddInfoTextId(TagHelperOutput); |
||||
|
|
||||
|
// Open and close button
|
||||
|
var openButtonContent = TagHelper.OpenButton |
||||
|
? await ProcessButtonAndGetContentAsync(context, output, "calendar", "open") |
||||
|
: ""; |
||||
|
var clearButtonContent = TagHelper.ClearButton |
||||
|
? await ProcessButtonAndGetContentAsync(context, output, "times", "clear", visible:!TagHelper.SingleOpenAndClearButton) |
||||
|
: ""; |
||||
|
|
||||
|
var labelContent = await GetLabelAsHtmlAsync(context, output, TagHelperOutput); |
||||
|
var infoContent = GetInfoAsHtml(context, output, TagHelperOutput); |
||||
|
var validationContent = await GetValidationAsHtmlAsync(context, output); |
||||
|
|
||||
|
var inputGroup = new TagHelperOutput("div", |
||||
|
new TagHelperAttributeList(new[] { new TagHelperAttribute("class", "input-group") }), |
||||
|
(_, _) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent())); |
||||
|
inputGroup.Content.AppendHtml( |
||||
|
TagHelperOutput.Render(Encoder) + openButtonContent + clearButtonContent |
||||
|
); |
||||
|
|
||||
|
var abpDatePickerTag = new TagHelperOutput(TagName, GetBaseTagAttributes(context, output, TagHelper), |
||||
|
(_, _) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent())); |
||||
|
abpDatePickerTag.Content.AppendHtml(inputGroup.Render(Encoder)); |
||||
|
abpDatePickerTag.Content.AppendHtml(validationContent); |
||||
|
abpDatePickerTag.Content.AppendHtml(GetExtraInputHtml(context, output)); |
||||
|
|
||||
|
var innerHtml = labelContent + abpDatePickerTag.Render(Encoder) + infoContent; |
||||
|
|
||||
|
var order = GetOrder(); |
||||
|
|
||||
|
AddGroupToFormGroupContents( |
||||
|
context, |
||||
|
GetPropertyName(), |
||||
|
SurroundInnerHtmlAndGet(context, output, innerHtml), |
||||
|
order |
||||
|
); |
||||
|
|
||||
|
|
||||
|
output.TagMode = TagMode.StartTagAndEndTag; |
||||
|
output.TagName = "div"; |
||||
|
LeaveOnlyGroupAttributes(context, output); |
||||
|
output.Attributes.AddClass("mb-3"); |
||||
|
|
||||
|
output.Content.AppendHtml(innerHtml); |
||||
|
} |
||||
|
|
||||
|
protected virtual void AddReadOnlyAttribute(TagHelperOutput inputTagHelperOutput) |
||||
|
{ |
||||
|
if (inputTagHelperOutput.Attributes.ContainsName("readonly") == false && |
||||
|
(TagHelper.IsReadonly != false || GetAttribute<ReadOnlyInput>() != null)) |
||||
|
{ |
||||
|
inputTagHelperOutput.Attributes.Add("readonly", ""); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected virtual TagHelperAttributeList GetInputAttributes(TagHelperContext context, TagHelperOutput output) |
||||
|
{ |
||||
|
var groupPrefix = "group-"; |
||||
|
|
||||
|
var tagHelperAttributes = output.Attributes.Where(a => !a.Name.StartsWith(groupPrefix)).ToList(); |
||||
|
|
||||
|
var attrList = new TagHelperAttributeList(); |
||||
|
|
||||
|
foreach (var tagHelperAttribute in tagHelperAttributes) |
||||
|
{ |
||||
|
attrList.Add(tagHelperAttribute); |
||||
|
} |
||||
|
|
||||
|
attrList.Add("type", "text"); |
||||
|
|
||||
|
if (attrList.ContainsName("value")) |
||||
|
{ |
||||
|
attrList.Remove(attrList.First(a => a.Name == "value")); |
||||
|
} |
||||
|
|
||||
|
if (!TagHelper.Name.IsNullOrEmpty() && !attrList.ContainsName("name")) |
||||
|
{ |
||||
|
attrList.Add("name", TagHelper.Name); |
||||
|
} |
||||
|
|
||||
|
if (!attrList.ContainsName("autocomplete")) |
||||
|
{ |
||||
|
attrList.Add("autocomplete", "off"); |
||||
|
} |
||||
|
|
||||
|
return attrList; |
||||
|
} |
||||
|
protected virtual void LeaveOnlyGroupAttributes(TagHelperContext context, TagHelperOutput output) |
||||
|
{ |
||||
|
var groupPrefix = "group-"; |
||||
|
var tagHelperAttributes = output.Attributes.Where(a => a.Name.StartsWith(groupPrefix)).ToList(); |
||||
|
|
||||
|
output.Attributes.Clear(); |
||||
|
|
||||
|
foreach (var tagHelperAttribute in tagHelperAttributes) |
||||
|
{ |
||||
|
var nameWithoutPrefix = tagHelperAttribute.Name.Substring(groupPrefix.Length); |
||||
|
var newAttribute = new TagHelperAttribute(nameWithoutPrefix, tagHelperAttribute.Value); |
||||
|
output.Attributes.Add(newAttribute); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected virtual string SurroundInnerHtmlAndGet(TagHelperContext context, TagHelperOutput output, string innerHtml) |
||||
|
{ |
||||
|
return "<div class=\"mb-3\">" + |
||||
|
Environment.NewLine + innerHtml + Environment.NewLine + |
||||
|
"</div>"; |
||||
|
} |
||||
|
|
||||
|
protected abstract string GetPropertyName(); |
||||
|
|
||||
|
protected virtual void AddGroupToFormGroupContents(TagHelperContext context, string propertyName, string html, |
||||
|
int order) |
||||
|
{ |
||||
|
var list = context.GetValue<List<FormGroupItem>>(FormGroupContents) ?? new List<FormGroupItem>(); |
||||
|
|
||||
|
if (!list.Any(igc => igc.HtmlContent.Contains("id=\"" + propertyName.Replace('.', '_') + "\""))) |
||||
|
{ |
||||
|
list.Add(new FormGroupItem { HtmlContent = html, Order = order, PropertyName = propertyName }); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected abstract int GetOrder(); |
||||
|
protected abstract void AddBaseTagAttributes(TagHelperAttributeList attributes); |
||||
|
|
||||
|
protected virtual string GetExtraInputHtml(TagHelperContext context, TagHelperOutput output) |
||||
|
{ |
||||
|
return string.Empty; |
||||
|
} |
||||
|
|
||||
|
protected TagHelperAttributeList ConvertDatePickerOptionsToAttributeList(IAbpDatePickerOptions options) |
||||
|
{ |
||||
|
var attrList = new TagHelperAttributeList(); |
||||
|
|
||||
|
if(options == null) |
||||
|
{ |
||||
|
return attrList; |
||||
|
} |
||||
|
|
||||
|
if (options.Locale != null) |
||||
|
{ |
||||
|
attrList.Add("data-locale", JsonSerializer.Serialize(options.Locale)); |
||||
|
} |
||||
|
|
||||
|
if (options.MinDate != null) |
||||
|
{ |
||||
|
attrList.Add("data-min-date", options.MinDate); |
||||
|
} |
||||
|
|
||||
|
if (options.MaxDate != null) |
||||
|
{ |
||||
|
attrList.Add("data-max-date", options.MaxDate); |
||||
|
} |
||||
|
|
||||
|
if (options.MaxSpan != null) |
||||
|
{ |
||||
|
attrList.Add("data-max-span", JsonSerializer.Serialize(options.MaxSpan)); |
||||
|
} |
||||
|
|
||||
|
if (options.ShowDropdowns == false) |
||||
|
{ |
||||
|
attrList.Add("data-show-dropdowns", options.ShowDropdowns.ToString().ToLowerInvariant()); |
||||
|
} |
||||
|
|
||||
|
if (options.MinYear != null) |
||||
|
{ |
||||
|
attrList.Add("data-min-year", options.MinYear); |
||||
|
} |
||||
|
|
||||
|
if (options.MaxYear != null) |
||||
|
{ |
||||
|
attrList.Add("data-max-year", options.MaxYear); |
||||
|
} |
||||
|
|
||||
|
switch (options.WeekNumbers) |
||||
|
{ |
||||
|
case AbpDatePickerWeekNumbers.Normal: |
||||
|
attrList.Add("data-show-week-numbers", "true"); |
||||
|
break; |
||||
|
case AbpDatePickerWeekNumbers.Iso: |
||||
|
attrList.Add("data-show-i-s-o-week-numbers", "true"); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
if (options.TimePicker != null) |
||||
|
{ |
||||
|
attrList.Add("data-time-picker", options.TimePicker.ToString().ToLowerInvariant()); |
||||
|
} |
||||
|
|
||||
|
if (options.TimePickerIncrement != null) |
||||
|
{ |
||||
|
attrList.Add("data-time-picker-increment", options.TimePickerIncrement); |
||||
|
} |
||||
|
|
||||
|
if (options.TimePicker24Hour != null) |
||||
|
{ |
||||
|
attrList.Add("data-time-picker24-hour", options.TimePicker24Hour.ToString().ToLowerInvariant()); |
||||
|
} |
||||
|
|
||||
|
if (options.TimePickerSeconds != null) |
||||
|
{ |
||||
|
attrList.Add("data-time-picker-seconds", options.TimePickerSeconds.ToString().ToLowerInvariant()); |
||||
|
} |
||||
|
|
||||
|
if (options.Opens != AbpDatePickerOpens.Center) |
||||
|
{ |
||||
|
attrList.Add("data-opens", options.Opens.ToString().ToLowerInvariant()); |
||||
|
} |
||||
|
|
||||
|
if (options.Drops != AbpDatePickerDrops.Down) |
||||
|
{ |
||||
|
attrList.Add("data-drops", options.Drops.ToString().ToLowerInvariant()); |
||||
|
} |
||||
|
|
||||
|
if (!options.ButtonClasses.IsNullOrEmpty()) |
||||
|
{ |
||||
|
attrList.Add("data-button-classes", options.ButtonClasses); |
||||
|
} |
||||
|
|
||||
|
if (!options.ApplyButtonClasses.IsNullOrEmpty()) |
||||
|
{ |
||||
|
attrList.Add("data-apply-button-classes", options.ApplyButtonClasses); |
||||
|
} |
||||
|
|
||||
|
if (!options.ClearButtonClasses.IsNullOrEmpty()) |
||||
|
{ |
||||
|
attrList.Add("data-clear-button-classes", options.ClearButtonClasses); |
||||
|
} |
||||
|
|
||||
|
if (!options.TodayButtonClasses.IsNullOrEmpty()) |
||||
|
{ |
||||
|
attrList.Add("data-today-button-classes", options.TodayButtonClasses); |
||||
|
} |
||||
|
|
||||
|
if (options.AutoApply != null) |
||||
|
{ |
||||
|
attrList.Add("data-auto-apply", options.AutoApply.ToString().ToLowerInvariant()); |
||||
|
} |
||||
|
|
||||
|
if (options.LinkedCalendars != null) |
||||
|
{ |
||||
|
attrList.Add("data-linked-calendars", options.LinkedCalendars.ToString().ToLowerInvariant()); |
||||
|
} |
||||
|
|
||||
|
if (options.AutoUpdateInput != null) |
||||
|
{ |
||||
|
attrList.Add("data-auto-update-input", options.AutoUpdateInput.ToString().ToLowerInvariant()); |
||||
|
} |
||||
|
|
||||
|
if (!options.ParentEl.IsNullOrEmpty()) |
||||
|
{ |
||||
|
attrList.Add("data-parent-el", options.ParentEl); |
||||
|
} |
||||
|
|
||||
|
if (!options.DateFormat.IsNullOrEmpty()) |
||||
|
{ |
||||
|
attrList.Add("data-date-format", options.DateFormat); |
||||
|
} |
||||
|
|
||||
|
if(options.Ranges != null && options.Ranges.Any()) |
||||
|
{ |
||||
|
var ranges = options.Ranges.ToDictionary(r => r.Label, r => r.Dates); |
||||
|
|
||||
|
attrList.Add("data-ranges", JsonSerializer.Serialize(ranges)); |
||||
|
} |
||||
|
|
||||
|
if(options.AlwaysShowCalendars != null) |
||||
|
{ |
||||
|
attrList.Add("data-always-show-calendars", options.AlwaysShowCalendars.ToString().ToLowerInvariant()); |
||||
|
} |
||||
|
|
||||
|
if(options.ShowCustomRangeLabel == false) |
||||
|
{ |
||||
|
attrList.Add("data-show-custom-range-label", options.ShowCustomRangeLabel.ToString().ToLowerInvariant()); |
||||
|
} |
||||
|
|
||||
|
if(options.Options != null) |
||||
|
{ |
||||
|
attrList.Add("data-options", JsonSerializer.Serialize(options.Options)); |
||||
|
} |
||||
|
|
||||
|
if (options.IsUtc != null) |
||||
|
{ |
||||
|
attrList.Add("data-is-utc", options.IsUtc.ToString().ToLowerInvariant()); |
||||
|
} |
||||
|
|
||||
|
if (options.IsIso != null) |
||||
|
{ |
||||
|
attrList.Add("data-is-iso", options.IsIso.ToString().ToLowerInvariant()); |
||||
|
} |
||||
|
|
||||
|
if (!options.PickerId.IsNullOrWhiteSpace()) |
||||
|
{ |
||||
|
attrList.Add("id", options.PickerId); |
||||
|
} |
||||
|
|
||||
|
if(!options.SingleOpenAndClearButton) |
||||
|
{ |
||||
|
attrList.Add("data-single-open-and-clear-button", options.SingleOpenAndClearButton.ToString().ToLowerInvariant()); |
||||
|
} |
||||
|
|
||||
|
return attrList; |
||||
|
} |
||||
|
|
||||
|
protected TagHelperAttributeList GetBaseTagAttributes(TagHelperContext context, TagHelperOutput output, IAbpDatePickerOptions options) |
||||
|
{ |
||||
|
var groupPrefix = "group-"; |
||||
|
|
||||
|
var tagHelperAttributes = output.Attributes.Where(a => !a.Name.StartsWith(groupPrefix)).ToList(); |
||||
|
|
||||
|
var attrList = new TagHelperAttributeList(); |
||||
|
|
||||
|
foreach (var tagHelperAttribute in tagHelperAttributes) |
||||
|
{ |
||||
|
attrList.Add(tagHelperAttribute); |
||||
|
} |
||||
|
|
||||
|
if (attrList.ContainsName("type")) |
||||
|
{ |
||||
|
attrList.Remove(attrList.First(a => a.Name == "type")); |
||||
|
} |
||||
|
|
||||
|
if (attrList.ContainsName("name")) |
||||
|
{ |
||||
|
attrList.Remove(attrList.First(a => a.Name == "name")); |
||||
|
} |
||||
|
|
||||
|
if (attrList.ContainsName("id")) |
||||
|
{ |
||||
|
attrList.Remove(attrList.First(a => a.Name == "id")); |
||||
|
} |
||||
|
|
||||
|
if (attrList.ContainsName("value")) |
||||
|
{ |
||||
|
attrList.Remove(attrList.First(a => a.Name == "value")); |
||||
|
} |
||||
|
|
||||
|
foreach (var attr in ConvertDatePickerOptionsToAttributeList(options)) |
||||
|
{ |
||||
|
attrList.Add(attr); |
||||
|
} |
||||
|
|
||||
|
var optionsAttribute = GetAttributeAndModelExpression<DatePickerOptionsAttribute>(out var modelExpression); |
||||
|
if (optionsAttribute != null) |
||||
|
{ |
||||
|
foreach (var attr in ConvertDatePickerOptionsToAttributeList(optionsAttribute.GetDatePickerOptions(modelExpression.ModelExplorer))) |
||||
|
{ |
||||
|
attrList.Add(attr); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
AddBaseTagAttributes(attrList); |
||||
|
|
||||
|
return attrList; |
||||
|
} |
||||
|
|
||||
|
protected virtual bool IsOutputHidden(TagHelperOutput inputTag) |
||||
|
{ |
||||
|
return inputTag.Attributes.Any(a => |
||||
|
a.Name.ToLowerInvariant() == "type" && a.Value?.ToString()?.ToLowerInvariant() == "hidden"); |
||||
|
} |
||||
|
|
||||
|
protected virtual string GetInfoAsHtml(TagHelperContext context, TagHelperOutput output, TagHelperOutput inputTag) |
||||
|
{ |
||||
|
if (IsOutputHidden(inputTag)) |
||||
|
{ |
||||
|
return string.Empty; |
||||
|
} |
||||
|
|
||||
|
string text; |
||||
|
ModelExplorer modelExplorer = null; |
||||
|
|
||||
|
if (!string.IsNullOrEmpty(TagHelper.InfoText)) |
||||
|
{ |
||||
|
text = TagHelper.InfoText; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var infoAttribute = GetAttributeAndModelExpression<InputInfoText>(out var modelExpression); |
||||
|
if (infoAttribute != null) |
||||
|
{ |
||||
|
modelExplorer = modelExpression.ModelExplorer; |
||||
|
text = infoAttribute.Text; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
return string.Empty; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var idAttr = inputTag.Attributes.FirstOrDefault(a => a.Name == "id"); |
||||
|
modelExplorer ??= GetModelExpression().ModelExplorer; |
||||
|
var localizedText = TagHelperLocalizer.GetLocalizedText(text, modelExplorer); |
||||
|
|
||||
|
var div = new TagBuilder("div"); |
||||
|
div.Attributes.Add("id", idAttr?.Value + "InfoText"); |
||||
|
div.AddCssClass("form-text"); |
||||
|
div.InnerHtml.Append(localizedText); |
||||
|
|
||||
|
inputTag.Attributes.Add("aria-describedby", idAttr?.Value + "InfoText"); |
||||
|
|
||||
|
return div.ToHtmlString(); |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task<string> GetLabelAsHtmlAsync(TagHelperContext context, TagHelperOutput output, |
||||
|
TagHelperOutput inputTag) |
||||
|
{ |
||||
|
if (IsOutputHidden(inputTag) || TagHelper.SuppressLabel) |
||||
|
{ |
||||
|
return string.Empty; |
||||
|
} |
||||
|
if (string.IsNullOrEmpty(TagHelper.Label)) |
||||
|
{ |
||||
|
return await GetLabelAsHtmlUsingTagHelperAsync(context, output) + GetRequiredSymbol(context, output); |
||||
|
} |
||||
|
|
||||
|
var label = new TagBuilder("label"); |
||||
|
label.Attributes.Add("for", GetIdAttributeValue(inputTag)); |
||||
|
label.InnerHtml.AppendHtml(TagHelper.Label); |
||||
|
|
||||
|
label.AddCssClass("form-label"); |
||||
|
|
||||
|
if (!TagHelper.LabelTooltip.IsNullOrEmpty()) |
||||
|
{ |
||||
|
label.Attributes.Add("data-bs-toggle", "tooltip"); |
||||
|
label.Attributes.Add("data-bs-placement", TagHelper.LabelTooltipPlacement); |
||||
|
if (TagHelper.LabelTooltipHtml) |
||||
|
{ |
||||
|
label.Attributes.Add("data-bs-html", "true"); |
||||
|
} |
||||
|
|
||||
|
label.Attributes.Add("title", TagHelper.LabelTooltip); |
||||
|
label.InnerHtml.AppendHtml($" <i class=\"bi {TagHelper.LabelTooltipIcon}\"></i>"); |
||||
|
} |
||||
|
|
||||
|
return label.ToHtmlString(); |
||||
|
} |
||||
|
|
||||
|
protected virtual string GetIdAttributeValue(TagHelperOutput inputTag) |
||||
|
{ |
||||
|
var idAttr = inputTag.Attributes.FirstOrDefault(a => a.Name == "id"); |
||||
|
|
||||
|
return idAttr != null ? idAttr.Value.ToString() : string.Empty; |
||||
|
} |
||||
|
|
||||
|
protected virtual string GetRequiredSymbol(TagHelperContext context, TagHelperOutput output) |
||||
|
{ |
||||
|
if (!TagHelper.DisplayRequiredSymbol) |
||||
|
{ |
||||
|
return ""; |
||||
|
} |
||||
|
|
||||
|
var isHaveRequiredAttribute = context.AllAttributes.Any(a => a.Name == "required"); |
||||
|
|
||||
|
return GetAttribute<RequiredAttribute>() != null || isHaveRequiredAttribute ? "<span> * </span>" : ""; |
||||
|
} |
||||
|
|
||||
|
protected abstract ModelExpression GetModelExpression(); |
||||
|
|
||||
|
protected virtual async Task<string> GetLabelAsHtmlUsingTagHelperAsync(TagHelperContext context, |
||||
|
TagHelperOutput output) |
||||
|
{ |
||||
|
var modelExpression = GetModelExpression(); |
||||
|
if (modelExpression == null) |
||||
|
{ |
||||
|
return string.Empty; |
||||
|
} |
||||
|
var labelTagHelper = new LabelTagHelper(Generator) { |
||||
|
ViewContext = TagHelper.ViewContext, |
||||
|
For = modelExpression |
||||
|
}; |
||||
|
|
||||
|
var attributeList = new TagHelperAttributeList(); |
||||
|
|
||||
|
attributeList.AddClass("form-label"); |
||||
|
|
||||
|
if (!TagHelper.LabelTooltip.IsNullOrEmpty()) |
||||
|
{ |
||||
|
attributeList.Add("data-bs-toggle", "tooltip"); |
||||
|
attributeList.Add("data-bs-placement", TagHelper.LabelTooltipPlacement); |
||||
|
if (TagHelper.LabelTooltipHtml) |
||||
|
{ |
||||
|
attributeList.Add("data-bs-html", "true"); |
||||
|
} |
||||
|
|
||||
|
attributeList.Add("title", TagHelper.LabelTooltip); |
||||
|
} |
||||
|
|
||||
|
var innerOutput = |
||||
|
await labelTagHelper.ProcessAndGetOutputAsync(attributeList, context, "label", TagMode.StartTagAndEndTag); |
||||
|
if (!TagHelper.LabelTooltip.IsNullOrEmpty()) |
||||
|
{ |
||||
|
innerOutput.Content.AppendHtml($" <i class=\"bi {TagHelper.LabelTooltipIcon}\"></i>"); |
||||
|
} |
||||
|
|
||||
|
return innerOutput.Render(Encoder); |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task<string> ProcessButtonAndGetContentAsync(TagHelperContext context, |
||||
|
TagHelperOutput output, string icon, string type, bool visible = true) |
||||
|
{ |
||||
|
var abpButtonTagHelper = ServiceProvider.GetRequiredService<AbpButtonTagHelper>(); |
||||
|
var attributes = |
||||
|
new TagHelperAttributeList { new("type", "button"), new("tabindex", "-1"), new("data-type", type) }; |
||||
|
abpButtonTagHelper.ButtonType = AbpButtonType.Outline_Secondary; |
||||
|
abpButtonTagHelper.Icon = icon; |
||||
|
|
||||
|
abpButtonTagHelper.Disabled = TagHelper.IsDisabled; |
||||
|
|
||||
|
if (!visible) |
||||
|
{ |
||||
|
attributes.AddClass("d-none"); |
||||
|
} |
||||
|
|
||||
|
return await abpButtonTagHelper.RenderAsync(attributes, context, Encoder, "button", TagMode.StartTagAndEndTag); |
||||
|
} |
||||
|
|
||||
|
protected virtual void AddInfoTextId(TagHelperOutput inputTagHelperOutput) |
||||
|
{ |
||||
|
if (GetAttribute<InputInfoText>() == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
var idAttr = inputTagHelperOutput.Attributes.FirstOrDefault(a => a.Name == "id"); |
||||
|
|
||||
|
if (idAttr == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
inputTagHelperOutput.Attributes.Add("aria-describedby", GetInfoText()); |
||||
|
} |
||||
|
|
||||
|
public virtual string GetInfoText() |
||||
|
{ |
||||
|
var infoAttribute = GetAttributeAndModelExpression<InputInfoText>(out var modelExpression); |
||||
|
|
||||
|
if (infoAttribute != null) |
||||
|
{ |
||||
|
return TagHelperLocalizer.GetLocalizedText(infoAttribute.Text, modelExpression.ModelExplorer); |
||||
|
} |
||||
|
|
||||
|
return string.Empty; |
||||
|
} |
||||
|
|
||||
|
protected virtual void AddPlaceholderAttribute(TagHelperOutput inputTagHelperOutput) |
||||
|
{ |
||||
|
if (inputTagHelperOutput.Attributes.ContainsName("placeholder")) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
var attribute = GetAttributeAndModelExpression<Placeholder>(out var modelExpression); |
||||
|
|
||||
|
if (attribute != null) |
||||
|
{ |
||||
|
var placeholderLocalized = |
||||
|
TagHelperLocalizer.GetLocalizedText(attribute.Value, modelExpression.ModelExplorer); |
||||
|
|
||||
|
inputTagHelperOutput.Attributes.Add("placeholder", placeholderLocalized); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected virtual void AddFormControls(TagHelperContext context, TagHelperOutput output, |
||||
|
TagHelperOutput inputTagHelperOutput) |
||||
|
{ |
||||
|
inputTagHelperOutput.Attributes.AddClass("form-control"); |
||||
|
var size = GetSize(context, output); |
||||
|
if (!size.IsNullOrEmpty()) |
||||
|
{ |
||||
|
inputTagHelperOutput.Attributes.AddClass(size); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected virtual void AddAutoFocusAttribute(TagHelperOutput inputTagHelperOutput) |
||||
|
{ |
||||
|
if (TagHelper.AutoFocus && !inputTagHelperOutput.Attributes.ContainsName("data-auto-focus")) |
||||
|
{ |
||||
|
inputTagHelperOutput.Attributes.Add("data-auto-focus", "true"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected virtual void AddDisabledAttribute(TagHelperOutput inputTagHelperOutput) |
||||
|
{ |
||||
|
if (inputTagHelperOutput.Attributes.ContainsName("disabled") == false && |
||||
|
(TagHelper.IsDisabled || GetAttribute<DisabledInput>() != null)) |
||||
|
{ |
||||
|
inputTagHelperOutput.Attributes.Add("disabled", ""); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
protected virtual string GetSize(TagHelperContext context, TagHelperOutput output) |
||||
|
{ |
||||
|
// TODO: Test this method
|
||||
|
var attribute = GetAttribute<FormControlSize>(); |
||||
|
|
||||
|
if (attribute != null) |
||||
|
{ |
||||
|
TagHelper.Size = attribute.Size; |
||||
|
} |
||||
|
|
||||
|
return TagHelper.Size switch { |
||||
|
AbpFormControlSize.Small => "form-control-sm", |
||||
|
AbpFormControlSize.Medium => "form-control-md", |
||||
|
AbpFormControlSize.Large => "form-control-lg", |
||||
|
_ => "" |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
protected abstract Task<string> GetValidationAsHtmlAsync(TagHelperContext context, TagHelperOutput output); |
||||
|
|
||||
|
protected virtual async Task<string> GetValidationAsHtmlByInputAsync(TagHelperContext context, |
||||
|
TagHelperOutput output, |
||||
|
[NotNull]InputTagHelper inputTag) |
||||
|
{ |
||||
|
var validationMessageTagHelper = |
||||
|
new ValidationMessageTagHelper(Generator) { For = inputTag.For, ViewContext = TagHelper.ViewContext }; |
||||
|
|
||||
|
var attributeList = new TagHelperAttributeList { { "class", "text-danger col-auto" } }; |
||||
|
|
||||
|
return await validationMessageTagHelper.RenderAsync(attributeList, context, Encoder, "span", |
||||
|
TagMode.StartTagAndEndTag); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,8 @@ |
|||||
|
namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form.DatePicker; |
||||
|
|
||||
|
public enum AbpDatePickerDrops : byte |
||||
|
{ |
||||
|
Down = 1, |
||||
|
Up = 2, |
||||
|
Auto = 3 |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form.DatePicker; |
||||
|
|
||||
|
public enum AbpDatePickerOpens : byte |
||||
|
{ |
||||
|
Left = 1, |
||||
|
Right = 2, |
||||
|
Center = 3, |
||||
|
} |
||||
@ -0,0 +1,41 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form.DatePicker; |
||||
|
|
||||
|
public class AbpDatePickerOptions : IAbpDatePickerOptions |
||||
|
{ |
||||
|
public string PickerId { get; set; } |
||||
|
public DateTime? MinDate { get; set; } |
||||
|
public DateTime? MaxDate { get; set; } |
||||
|
public object MaxSpan { get; set; } |
||||
|
public bool? ShowDropdowns { get; set; } |
||||
|
public int? MinYear { get; set; } |
||||
|
public int? MaxYear { get; set; } |
||||
|
public AbpDatePickerWeekNumbers WeekNumbers { get; set; } = AbpDatePickerWeekNumbers.None; |
||||
|
public bool? TimePicker { get; set; } |
||||
|
public int? TimePickerIncrement { get; set; } |
||||
|
public bool? TimePicker24Hour { get; set; } |
||||
|
public bool? TimePickerSeconds { get; set; } |
||||
|
public List<AbpDatePickerRange> Ranges { get; set; } |
||||
|
public bool? ShowCustomRangeLabel { get; set; } |
||||
|
public bool? AlwaysShowCalendars { get; set; } |
||||
|
public AbpDatePickerOpens Opens { get; set; } = AbpDatePickerOpens.Center; |
||||
|
public AbpDatePickerDrops Drops { get; set; } = AbpDatePickerDrops.Down; |
||||
|
public string ButtonClasses { get; set; } |
||||
|
public string TodayButtonClasses { get; set; } |
||||
|
public string ApplyButtonClasses { get; set; } |
||||
|
public string ClearButtonClasses { get; set; } |
||||
|
public object Locale { get; set; } |
||||
|
public bool? AutoApply { get; set; } |
||||
|
public bool? LinkedCalendars { get; set; } |
||||
|
public bool? AutoUpdateInput { get; set; } |
||||
|
public string ParentEl { get; set; } |
||||
|
public string DateFormat { get; set; } |
||||
|
public bool OpenButton { get; set; } = true; |
||||
|
public bool ClearButton { get; set; } = true; |
||||
|
public bool SingleOpenAndClearButton { get; set; } = true; |
||||
|
public bool? IsUtc { get; set; } |
||||
|
public bool? IsIso { get; set; } |
||||
|
public object Options { get; set; } |
||||
|
} |
||||
@ -0,0 +1,90 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form.DatePicker; |
||||
|
|
||||
|
public class AbpDatePickerRange |
||||
|
{ |
||||
|
private readonly List<string> _dates = new List<string>(); |
||||
|
public string Label { get; set; } |
||||
|
public IReadOnlyList<string> Dates => _dates; |
||||
|
|
||||
|
public AbpDatePickerRange() |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
public AbpDatePickerRange(string label, DateTime start, DateTime end) |
||||
|
{ |
||||
|
Label = label; |
||||
|
AddDate(start); |
||||
|
AddDate(end); |
||||
|
} |
||||
|
|
||||
|
public AbpDatePickerRange(string label, DateTime date) |
||||
|
{ |
||||
|
Label = label; |
||||
|
AddDate(date); |
||||
|
} |
||||
|
|
||||
|
public AbpDatePickerRange(string label, DateTimeOffset start, DateTimeOffset end) |
||||
|
{ |
||||
|
Label = label; |
||||
|
AddDate(start); |
||||
|
AddDate(end); |
||||
|
} |
||||
|
|
||||
|
public AbpDatePickerRange(string label, DateTimeOffset date) |
||||
|
{ |
||||
|
Label = label; |
||||
|
AddDate(date); |
||||
|
} |
||||
|
|
||||
|
public AbpDatePickerRange(string label, string start, string end) |
||||
|
{ |
||||
|
Label = label; |
||||
|
AddDate(start); |
||||
|
AddDate(end); |
||||
|
} |
||||
|
|
||||
|
public AbpDatePickerRange(string label, string date) |
||||
|
{ |
||||
|
Label = label; |
||||
|
AddDate(date); |
||||
|
} |
||||
|
|
||||
|
public void AddDate(string date) |
||||
|
{ |
||||
|
_dates.Add(DateTime.Parse(date).ToString("O")); |
||||
|
} |
||||
|
|
||||
|
public void AddDate(DateTime date) |
||||
|
{ |
||||
|
_dates.Add(date.ToString("O")); |
||||
|
} |
||||
|
|
||||
|
public void AddDate(DateTimeOffset date) |
||||
|
{ |
||||
|
_dates.Add(date.ToString("O")); |
||||
|
} |
||||
|
|
||||
|
public void AddDate(DateTime? date) |
||||
|
{ |
||||
|
if (date.HasValue) |
||||
|
{ |
||||
|
_dates.Add(date.Value.ToString("O")); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void AddDate(DateTimeOffset? date) |
||||
|
{ |
||||
|
if (date.HasValue) |
||||
|
{ |
||||
|
_dates.Add(date.Value.ToString("O")); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void AddDate(string date, string format) |
||||
|
{ |
||||
|
_dates.Add(DateTime.ParseExact(date, format, null).ToString("O")); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
using JetBrains.Annotations; |
||||
|
using Microsoft.AspNetCore.Mvc.ViewFeatures; |
||||
|
using Microsoft.AspNetCore.Razor.TagHelpers; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form.DatePicker; |
||||
|
|
||||
|
[HtmlTargetElement("abp-date-picker", TagStructure = TagStructure.NormalOrSelfClosing)] |
||||
|
public class AbpDatePickerTagHelper : AbpDatePickerBaseTagHelper<AbpDatePickerTagHelper> |
||||
|
{ |
||||
|
[CanBeNull] |
||||
|
public ModelExpression AspFor { get; set; } |
||||
|
|
||||
|
public AbpDatePickerTagHelper(AbpDatePickerTagHelperService service) : base(service) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,88 @@ |
|||||
|
using System; |
||||
|
using System.Text.Encodings.Web; |
||||
|
using System.Threading.Tasks; |
||||
|
using JetBrains.Annotations; |
||||
|
using Localization.Resources.AbpUi; |
||||
|
using Microsoft.AspNetCore.Mvc.TagHelpers; |
||||
|
using Microsoft.AspNetCore.Mvc.ViewFeatures; |
||||
|
using Microsoft.AspNetCore.Razor.TagHelpers; |
||||
|
using Microsoft.Extensions.Localization; |
||||
|
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Extensions; |
||||
|
using Volo.Abp.Json; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form.DatePicker; |
||||
|
|
||||
|
public class AbpDatePickerTagHelperService : AbpDatePickerBaseTagHelperService<AbpDatePickerTagHelper> |
||||
|
{ |
||||
|
public AbpDatePickerTagHelperService(IJsonSerializer jsonSerializer, IHtmlGenerator generator, HtmlEncoder encoder, IServiceProvider serviceProvider, IStringLocalizer<AbpUiResource> l, IAbpTagHelperLocalizer tagHelperLocalizer) : base(jsonSerializer, generator, encoder, serviceProvider, l, tagHelperLocalizer) |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
|
||||
|
protected override TagHelperOutput TagHelperOutput { get; set; } |
||||
|
|
||||
|
[CanBeNull] |
||||
|
protected virtual InputTagHelper DateTagHelper { get; set; } |
||||
|
|
||||
|
[CanBeNull] |
||||
|
protected virtual TagHelperOutput DateTagHelperOutput { get; set; } |
||||
|
protected override string GetPropertyName() |
||||
|
{ |
||||
|
return TagHelper.AspFor?.Name ?? string.Empty; |
||||
|
} |
||||
|
|
||||
|
protected override T GetAttributeAndModelExpression<T>(out ModelExpression modelExpression) |
||||
|
{ |
||||
|
modelExpression = TagHelper.AspFor; |
||||
|
return modelExpression?.ModelExplorer.GetAttribute<T>(); |
||||
|
} |
||||
|
|
||||
|
public async override Task ProcessAsync(TagHelperContext context, TagHelperOutput output) |
||||
|
{ |
||||
|
if (TagHelper.AspFor != null) |
||||
|
{ |
||||
|
DateTagHelper = new InputTagHelper(Generator) |
||||
|
{ |
||||
|
InputTypeName = "hidden", |
||||
|
ViewContext = TagHelper.ViewContext, |
||||
|
For = TagHelper.AspFor, |
||||
|
}; |
||||
|
|
||||
|
var attributes = new TagHelperAttributeList { { "data-date", "true" }, { "type", "hidden" } }; |
||||
|
DateTagHelperOutput = await DateTagHelper.ProcessAndGetOutputAsync(attributes, context, "input"); |
||||
|
} |
||||
|
|
||||
|
await base.ProcessAsync(context, output); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
protected override int GetOrder() |
||||
|
{ |
||||
|
return TagHelper.AspFor?.Metadata.Order ?? 0; |
||||
|
} |
||||
|
|
||||
|
protected override void AddBaseTagAttributes(TagHelperAttributeList attributes) |
||||
|
{ |
||||
|
if (TagHelper.AspFor != null && |
||||
|
TagHelper.AspFor.Model != null && |
||||
|
SupportedInputTypes.TryGetValue(TagHelper.AspFor.Metadata.ModelType, out var convertFunc)) |
||||
|
{ |
||||
|
attributes.Add("data-date", convertFunc(TagHelper.AspFor.Model)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected override ModelExpression GetModelExpression() |
||||
|
{ |
||||
|
return TagHelper.AspFor; |
||||
|
} |
||||
|
|
||||
|
protected async override Task<string> GetValidationAsHtmlAsync(TagHelperContext context, TagHelperOutput output) |
||||
|
{ |
||||
|
return DateTagHelper != null ? await GetValidationAsHtmlByInputAsync(context, output, DateTagHelper) : string.Empty; |
||||
|
} |
||||
|
|
||||
|
protected override string GetExtraInputHtml(TagHelperContext context, TagHelperOutput output) |
||||
|
{ |
||||
|
return DateTagHelperOutput?.Render(Encoder); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,8 @@ |
|||||
|
namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form.DatePicker; |
||||
|
|
||||
|
public enum AbpDatePickerWeekNumbers : byte |
||||
|
{ |
||||
|
None = 0, |
||||
|
Normal = 1, |
||||
|
Iso = 2 |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
using JetBrains.Annotations; |
||||
|
using Microsoft.AspNetCore.Mvc.ViewFeatures; |
||||
|
using Microsoft.AspNetCore.Razor.TagHelpers; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form.DatePicker; |
||||
|
|
||||
|
[HtmlTargetElement("abp-date-range-picker", TagStructure = TagStructure.NormalOrSelfClosing)] |
||||
|
public class AbpDateRangePickerTagHelper : AbpDatePickerBaseTagHelper<AbpDateRangePickerTagHelper> |
||||
|
{ |
||||
|
[CanBeNull] |
||||
|
public ModelExpression AspForStart { get; set; } |
||||
|
|
||||
|
[CanBeNull] |
||||
|
public ModelExpression AspForEnd { get; set; } |
||||
|
|
||||
|
public AbpDateRangePickerTagHelper(AbpDateRangePickerTagHelperService tagHelperService) : |
||||
|
base(tagHelperService) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,132 @@ |
|||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using System.Text.Encodings.Web; |
||||
|
using System.Threading.Tasks; |
||||
|
using JetBrains.Annotations; |
||||
|
using Localization.Resources.AbpUi; |
||||
|
using Microsoft.AspNetCore.Mvc.TagHelpers; |
||||
|
using Microsoft.AspNetCore.Mvc.ViewFeatures; |
||||
|
using Microsoft.AspNetCore.Razor.TagHelpers; |
||||
|
using Microsoft.Extensions.Localization; |
||||
|
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Extensions; |
||||
|
using Volo.Abp.Json; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form.DatePicker; |
||||
|
|
||||
|
public class AbpDateRangePickerTagHelperService : AbpDatePickerBaseTagHelperService<AbpDateRangePickerTagHelper> |
||||
|
{ |
||||
|
public AbpDateRangePickerTagHelperService(IJsonSerializer jsonSerializer, IHtmlGenerator generator, |
||||
|
HtmlEncoder encoder, IServiceProvider serviceProvider, IStringLocalizer<AbpUiResource> l, |
||||
|
IAbpTagHelperLocalizer tagHelperLocalizer) : |
||||
|
base(jsonSerializer, generator, encoder, serviceProvider, l, |
||||
|
tagHelperLocalizer) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
protected override string TagName { get; set; } = "abp-date-range-picker"; |
||||
|
|
||||
|
protected override T GetAttributeAndModelExpression<T>(out ModelExpression modelExpression) |
||||
|
{ |
||||
|
modelExpression = new[] { TagHelper.AspForStart, TagHelper.AspForEnd }.FirstOrDefault(x => x?.ModelExplorer?.GetAttribute<T>() != null); |
||||
|
return modelExpression?.ModelExplorer.GetAttribute<T>(); |
||||
|
} |
||||
|
|
||||
|
public async override Task ProcessAsync(TagHelperContext context, TagHelperOutput output) |
||||
|
{ |
||||
|
if (TagHelper.AspForStart != null) |
||||
|
{ |
||||
|
var startDateAttributes = new TagHelperAttributeList { { "data-start-date", "true" }, { "type", "hidden" } }; |
||||
|
StartDateTagHelper = new InputTagHelper(Generator) |
||||
|
{ |
||||
|
ViewContext = TagHelper.ViewContext, |
||||
|
For = TagHelper.AspForStart, |
||||
|
InputTypeName = "hidden" |
||||
|
}; |
||||
|
|
||||
|
StartDateTagHelperOutput = await StartDateTagHelper.ProcessAndGetOutputAsync(startDateAttributes, context, "input"); |
||||
|
} |
||||
|
|
||||
|
if (TagHelper.AspForEnd != null) |
||||
|
{ |
||||
|
var endDateAttributes = new TagHelperAttributeList { { "data-end-date", "true" }, { "type", "hidden" } }; |
||||
|
EndDateTagHelper = new InputTagHelper(Generator) |
||||
|
{ |
||||
|
ViewContext = TagHelper.ViewContext, |
||||
|
For = TagHelper.AspForEnd, |
||||
|
InputTypeName = "hidden" |
||||
|
}; |
||||
|
|
||||
|
EndDateTagHelperOutput = await EndDateTagHelper.ProcessAndGetOutputAsync(endDateAttributes, context, "input"); |
||||
|
} |
||||
|
|
||||
|
await base.ProcessAsync(context, output); |
||||
|
} |
||||
|
|
||||
|
protected override TagHelperOutput TagHelperOutput { get; set; } |
||||
|
|
||||
|
[CanBeNull] |
||||
|
protected virtual InputTagHelper StartDateTagHelper { get; set; } |
||||
|
|
||||
|
[CanBeNull] |
||||
|
protected virtual TagHelperOutput StartDateTagHelperOutput { get; set; } |
||||
|
|
||||
|
[CanBeNull] |
||||
|
protected virtual InputTagHelper EndDateTagHelper { get; set; } |
||||
|
|
||||
|
[CanBeNull] |
||||
|
protected virtual TagHelperOutput EndDateTagHelperOutput { get; set; } |
||||
|
|
||||
|
protected override string GetPropertyName() |
||||
|
{ |
||||
|
return TagHelper.AspForStart?.Name ?? string.Empty; |
||||
|
} |
||||
|
|
||||
|
protected override int GetOrder() |
||||
|
{ |
||||
|
return TagHelper.Order; |
||||
|
} |
||||
|
|
||||
|
protected override void AddBaseTagAttributes(TagHelperAttributeList attributes) |
||||
|
{ |
||||
|
if (TagHelper.AspForStart != null && |
||||
|
TagHelper.AspForStart.Model != null && |
||||
|
SupportedInputTypes.TryGetValue(TagHelper.AspForStart.Metadata.ModelType, out var convertFuncStart)) |
||||
|
{ |
||||
|
attributes.Add("data-start-date", convertFuncStart(TagHelper.AspForStart.Model)); |
||||
|
} |
||||
|
|
||||
|
if (TagHelper.AspForEnd != null && |
||||
|
TagHelper.AspForEnd.Model != null && |
||||
|
SupportedInputTypes.TryGetValue(TagHelper.AspForEnd.Metadata.ModelType, out var convertFuncEnd)) |
||||
|
{ |
||||
|
attributes.Add("data-end-date", convertFuncEnd(TagHelper.AspForEnd.Model)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected override string GetExtraInputHtml(TagHelperContext context, TagHelperOutput output) |
||||
|
{ |
||||
|
return StartDateTagHelperOutput?.Render(Encoder) + EndDateTagHelperOutput?.Render(Encoder); |
||||
|
} |
||||
|
|
||||
|
protected override ModelExpression GetModelExpression() |
||||
|
{ |
||||
|
return TagHelper.AspForStart; |
||||
|
} |
||||
|
|
||||
|
protected async override Task<string> GetValidationAsHtmlAsync(TagHelperContext context, TagHelperOutput output) |
||||
|
{ |
||||
|
var validationHtml = string.Empty; |
||||
|
|
||||
|
if (StartDateTagHelper != null) |
||||
|
{ |
||||
|
validationHtml += await GetValidationAsHtmlByInputAsync(context, output, StartDateTagHelper); |
||||
|
} |
||||
|
|
||||
|
if (EndDateTagHelper != null) |
||||
|
{ |
||||
|
validationHtml += await GetValidationAsHtmlByInputAsync(context, output, EndDateTagHelper); |
||||
|
} |
||||
|
|
||||
|
return validationHtml; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,8 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form.DatePicker; |
||||
|
|
||||
|
[AttributeUsage(AttributeTargets.Property)] |
||||
|
public class DatePickerAttribute : Attribute |
||||
|
{ |
||||
|
} |
||||
@ -0,0 +1,34 @@ |
|||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using Microsoft.AspNetCore.Mvc.ViewFeatures; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form.DatePicker; |
||||
|
|
||||
|
[AttributeUsage(AttributeTargets.Property)] |
||||
|
public class DatePickerOptionsAttribute : Attribute |
||||
|
{ |
||||
|
public string DatePickerOptionsPropertyName { get; set; } |
||||
|
|
||||
|
public DatePickerOptionsAttribute(string datePickerOptionsPropertyName) |
||||
|
{ |
||||
|
DatePickerOptionsPropertyName = datePickerOptionsPropertyName; |
||||
|
} |
||||
|
|
||||
|
public IAbpDatePickerOptions GetDatePickerOptions(ModelExplorer explorer) |
||||
|
{ |
||||
|
var properties = explorer.Container.Properties.Where(p => p.Metadata.PropertyName != null && p.Metadata.PropertyName.Equals(DatePickerOptionsPropertyName)).ToList(); |
||||
|
|
||||
|
while (properties.Count == 0) |
||||
|
{ |
||||
|
explorer = explorer.Container; |
||||
|
if(explorer.Container == null) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
properties = explorer.Container.Properties.Where(p => p.Metadata.PropertyName != null && p.Metadata.PropertyName.Equals(DatePickerOptionsPropertyName)).ToList(); |
||||
|
} |
||||
|
|
||||
|
return properties.FirstOrDefault(p=> p.Model is IAbpDatePickerOptions)?.Model as IAbpDatePickerOptions; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form.DatePicker; |
||||
|
|
||||
|
[AttributeUsage(AttributeTargets.Property)] |
||||
|
public class DateRangePickerAttribute : Attribute |
||||
|
{ |
||||
|
public string PickerId { get; set; } |
||||
|
|
||||
|
public bool IsStart { get; set; } |
||||
|
|
||||
|
public bool IsEnd => !IsStart; |
||||
|
|
||||
|
public DateRangePickerAttribute(string pickerId, bool isStart = false) |
||||
|
{ |
||||
|
PickerId = pickerId; |
||||
|
IsStart = isStart; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,160 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using JetBrains.Annotations; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form.DatePicker; |
||||
|
|
||||
|
public interface IAbpDatePickerOptions |
||||
|
{ |
||||
|
public string PickerId { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Min date allowed
|
||||
|
/// </summary>
|
||||
|
DateTime? MinDate { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Max date allowed
|
||||
|
/// </summary>
|
||||
|
DateTime? MaxDate { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The maximum span between the selected start and end dates. Can have any property you can add to a moment object (i.e. days, months)
|
||||
|
/// </summary>
|
||||
|
object MaxSpan { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Show year and month select boxes above calendars to jump to a specific month and year.
|
||||
|
/// </summary>
|
||||
|
bool? ShowDropdowns { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The minimum year shown in the dropdowns when showDropdowns is set to true.
|
||||
|
/// </summary>
|
||||
|
int? MinYear { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The maximum year shown in the dropdowns when showDropdowns is set to true.
|
||||
|
/// </summary>
|
||||
|
int? MaxYear { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Show week numbers at the start of each week on the calendars.
|
||||
|
/// </summary>
|
||||
|
AbpDatePickerWeekNumbers WeekNumbers { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Adds select boxes to choose times in addition to dates.
|
||||
|
/// </summary>
|
||||
|
bool? TimePicker { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Increment of the minutes selection list for times (i.e. 30 to allow only selection of times ending in 0 or 30).
|
||||
|
/// </summary>
|
||||
|
int? TimePickerIncrement { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Use 24-hour instead of 12-hour times, removing the AM/PM selection.
|
||||
|
/// </summary>
|
||||
|
bool? TimePicker24Hour { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Show seconds in the timePicker.
|
||||
|
/// </summary>
|
||||
|
bool? TimePickerSeconds { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Set predefined date ranges the user can select from. Each key is the label for the range, and its value an array with two dates representing the bounds of the range.
|
||||
|
/// </summary>
|
||||
|
List<AbpDatePickerRange> Ranges { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Displays "Custom Range" at the end of the list of predefined ranges, when the ranges option is used. This option will be highlighted whenever the current date range selection does not match one of the predefined ranges. Clicking it will display the calendars to select a new range.
|
||||
|
/// </summary>
|
||||
|
bool? ShowCustomRangeLabel { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Normally, if you use the ranges option to specify pre-defined date ranges, calendars for choosing a custom date range are not shown until the user clicks "Custom Range". When this option is set to true, the calendars for choosing a custom date range are always shown instead.
|
||||
|
/// </summary>
|
||||
|
bool? AlwaysShowCalendars { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Whether the picker appears aligned to the left, to the right, or centered under the HTML element it's attached to.
|
||||
|
/// </summary>
|
||||
|
AbpDatePickerOpens Opens { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Whether the picker appears below (default) or above the HTML element it's attached to.
|
||||
|
/// </summary>
|
||||
|
AbpDatePickerDrops Drops { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// CSS class names that will be added to both the today, clear, and apply buttons.
|
||||
|
/// </summary>
|
||||
|
[CanBeNull] |
||||
|
string ButtonClasses { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// CSS class names that will be added only to the today button.
|
||||
|
/// </summary>
|
||||
|
[CanBeNull] |
||||
|
string TodayButtonClasses { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// CSS class names that will be added only to the apply button.
|
||||
|
/// </summary>
|
||||
|
[CanBeNull] |
||||
|
string ApplyButtonClasses { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// CSS class names that will be added only to the clear button.
|
||||
|
/// </summary>
|
||||
|
[CanBeNull] |
||||
|
string ClearButtonClasses { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Allows you to provide localized strings for buttons and labels, customize the date format, and change the first day of week for the calendars.
|
||||
|
/// </summary>
|
||||
|
[CanBeNull] |
||||
|
object Locale { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Hide the apply button, and automatically apply a new date range as soon as two dates are clicked.
|
||||
|
/// </summary>
|
||||
|
bool? AutoApply { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// When enabled, the two calendars displayed will always be for two sequential months (i.e. January and February), and both will be advanced when clicking the left or right arrows above the calendars. When disabled, the two calendars can be individually advanced and display any month/year.
|
||||
|
/// </summary>
|
||||
|
bool? LinkedCalendars { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Indicates whether the date range picker should automatically update the value of the input element it's attached to at initialization and when the selected dates change.
|
||||
|
/// </summary>
|
||||
|
bool? AutoUpdateInput { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// jQuery selector of the parent element that the date range picker will be added to, if not provided this will be 'body'
|
||||
|
/// </summary>
|
||||
|
[CanBeNull] |
||||
|
string ParentEl { get; set; } |
||||
|
|
||||
|
[CanBeNull] |
||||
|
string DateFormat { get; set; } |
||||
|
|
||||
|
bool OpenButton { get; set; } |
||||
|
|
||||
|
bool ClearButton { get; set; } |
||||
|
|
||||
|
bool SingleOpenAndClearButton { get; set; } |
||||
|
|
||||
|
bool? IsUtc { get; set; } |
||||
|
|
||||
|
bool? IsIso { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Other non-mapped attributes will be automatically added to the input element as is.
|
||||
|
/// </summary>
|
||||
|
[CanBeNull] |
||||
|
object Options { get; set; } |
||||
|
} |
||||